/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
 * Copyright 2025 GNOME Foundation, Inc.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *  - Ignacy Kuchciński <ignacykuchcinski@gnome.org>
 */

#include "config.h"

#include <glib/gi18n-lib.h>
#include <libmalcontent/manager.h>

#include "time-page.h"
#include "cc-duration-row.h"
#include "screen-time-statistics-row.h"
#include "cc-time-row.h"

static gboolean update_session_limits_cb (gpointer data);

/**
 * MctTimePage:
 *
 * A widget which shows parental controls for screen time limits
 * for the selected user.
 *
 * [property@Malcontent.TimePage:user] may be `NULL`, in which case the
 * contents of the widget are undefined and it should not be shown.
 *
 * Since: 0.14.0
 */
struct _MctTimePage
{
  AdwNavigationPage parent;

  AdwWindowTitle *time_window_title;
  AdwPreferencesGroup *preferences_group;
  AdwPreferencesPage *preferences_page;
  AdwSwitchRow *screen_time_limit_row;
  AdwSwitchRow *bedtime_schedule_row;
  CcDurationRow *daily_time_limit_row;
  CcTimeRow *bedtime_row;

  unsigned int update_session_limits_source_id;
  unsigned long session_limits_changed_id;
  unsigned long user_notify_id;
  gboolean flushed_on_dispose;
  GCancellable *cancellable; /* (owned) */
  MctSessionLimits *limits; /* (owned) */
  MctSessionLimits *last_saved_limits; /* (owned); updated each time we internally time out and save the session limits */
  MctScreenTimeStatisticsRow *row;  /* (nullable) */

  MctUser *user; /* (owned) (nullable) */
  GDBusConnection *connection; /* (owned) */
  MctManager *policy_manager; /* (owned) */
};

G_DEFINE_TYPE (MctTimePage, mct_time_page, ADW_TYPE_NAVIGATION_PAGE)

typedef enum
{
  PROP_USER = 1,
  PROP_CONNECTION,
  PROP_POLICY_MANAGER,
} MctTimePageProperty;

static GParamSpec *properties[PROP_POLICY_MANAGER + 1];

static void
schedule_update_session_limits (MctTimePage *self)
{
  if (self->update_session_limits_source_id > 0)
    return;

  /* Use a timeout to batch multiple quick changes into a single
   * update. 1 second is an arbitrary sufficiently small number */
  self->update_session_limits_source_id =
      g_timeout_add_seconds (1, update_session_limits_cb, self);
}

static void
flush_update_session_limits (MctTimePage *self)
{
  if (self->update_session_limits_source_id > 0)
    {
      /* Remove the timer and forcefully call the timer callback. */
      g_source_remove (self->update_session_limits_source_id);
      self->update_session_limits_source_id = 0;

      update_session_limits_cb (self);
    }
}

static void
setup_time_page (MctTimePage *self)
{
  gboolean daily_limit, daily_schedule;
  unsigned int limit_secs, bedtime_secs;

  daily_limit = mct_session_limits_get_daily_limit (self->limits, &limit_secs);
  /* By default recommend a 1 hour daily screen time limit.
   * See https://www.familyeducation.com/entertainment-activities/online/screen-time-recommendations-by-age-chart. */
  if (limit_secs == 0)
    {
      limit_secs = 60 * 60;
    }

  daily_schedule = mct_session_limits_get_daily_schedule (self->limits,
                                                          NULL,
                                                          &bedtime_secs);
  if (bedtime_secs == 0)
    {
      /* Set a default bedtime of 20:00 */
      bedtime_secs = 20 * 60 * 60;
    }

  adw_switch_row_set_active (self->screen_time_limit_row, daily_limit);
  adw_switch_row_set_active (self->bedtime_schedule_row, daily_schedule);
  cc_duration_row_set_duration (self->daily_time_limit_row, limit_secs / 60);
  cc_time_row_set_time (self->bedtime_row, bedtime_secs / 60);
}

static void
get_session_limits_cb (GObject      *obj,
                       GAsyncResult *result,
                       gpointer      user_data)
{
  MctManager *policy_manager = MCT_MANAGER (obj);
  MctTimePage *self;
  g_autoptr(MctSessionLimits) new_session_limits = NULL;
  g_autoptr(GError) local_error = NULL;

  /* This may be called after MctTimePage has been finalised, in an idle
   * callback from cancelling the async call. So we can’t deref `user_data`
   * until we’ve checked for cancellation. */

  new_session_limits = mct_manager_get_session_limits_finish (policy_manager,
                                                              result,
                                                              &local_error);
  if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
    return;

  self = MCT_TIME_PAGE (user_data);

  g_clear_pointer (&self->limits, mct_session_limits_unref);
  g_clear_pointer (&self->last_saved_limits, mct_session_limits_unref);
  self->limits = g_steal_pointer (&new_session_limits);

  if (local_error != NULL)
    {
      g_warning ("Error retrieving session limits for user '%s': %s",
                 mct_user_get_username (self->user),
                 local_error->message);
      return;
    }

  self->last_saved_limits = mct_session_limits_ref (self->limits);

  g_debug ("Retrieved new session limits for user '%s'",
           mct_user_get_username (self->user));

  setup_time_page (self);
}

static void
set_session_limits_cb (GObject      *obj,
                       GAsyncResult *result,
                       gpointer      user_data)
{
  MctManager *policy_manager = MCT_MANAGER (obj);
  MctTimePage *self;
  g_autoptr(GError) local_error = NULL;

  /* This may be called after MctTimePage has been finalised, in an idle
   * callback from cancelling the async call. So we can’t deref `user_data`
   * until we’ve checked for cancellation. */

  if (!mct_manager_set_session_limits_finish (policy_manager,
                                              result,
                                              &local_error))
    {
      if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

      self = MCT_TIME_PAGE (user_data);

      g_warning ("Error updating session limits: %s", local_error->message);
      setup_time_page (self);
    }
}

static gboolean
update_session_limits_cb (gpointer data)
{
  g_auto(MctSessionLimitsBuilder) builder = MCT_SESSION_LIMITS_BUILDER_INIT ();
  g_autoptr(MctSessionLimits) new_limits = NULL;
  g_autoptr(GError) error = NULL;
  MctTimePage *self = data;
  guint limit, daily_schedule_start, daily_schedule_end, bedtime_minutes, bedtime_hours;
  gboolean screen_time_limit, bedtime_schedule;

  self->update_session_limits_source_id = 0;

  screen_time_limit = adw_switch_row_get_active (self->screen_time_limit_row);
  limit = cc_duration_row_get_duration (self->daily_time_limit_row);
  mct_session_limits_builder_set_daily_limit (&builder, screen_time_limit, limit * 60);

  bedtime_schedule = adw_switch_row_get_active (self->bedtime_schedule_row);

  bedtime_minutes = cc_time_row_get_time (self->bedtime_row);
  bedtime_hours = bedtime_minutes / 60;
  const unsigned int sleep_time_hours = 6;
  const unsigned int midnight = 24;

  /* Calculate the start and the end of the daily schedule in seconds.
   * Ensure at least 6 hours of sleep time for late bedtimes. For early
   * bedtimes, the start of the daily schedule would need to be after
   * the end, but split days aren't supported yet, so set it to zero
   * for now. Also  make sure the end of the daily schedule is non zero.*/
  if (bedtime_hours + sleep_time_hours >= midnight)
    daily_schedule_start = (bedtime_hours + sleep_time_hours) % midnight * 60 * 60;
  else
    daily_schedule_start = 0;

  if (bedtime_minutes != 0)
    daily_schedule_end = bedtime_minutes * 60;
  else
    daily_schedule_end = 1;

  mct_session_limits_builder_set_daily_schedule (&builder,
                                                 bedtime_schedule,
                                                 daily_schedule_start,
                                                 daily_schedule_end);

  new_limits = mct_session_limits_builder_end (&builder);

  /* Don't bother saving the session limit (which could result in asking the
   * user for admin permission) if it hasn't changed. */
  if (self->last_saved_limits != NULL &&
      mct_session_limits_equal (new_limits, self->last_saved_limits))
    {
      g_debug ("Not saving session limits as they haven't changed");
      return G_SOURCE_REMOVE;
    }

  mct_manager_set_session_limits_async (self->policy_manager,
                                        mct_user_get_uid (self->user),
                                        new_limits,
                                        MCT_MANAGER_SET_VALUE_FLAGS_INTERACTIVE,
                                        self->cancellable,
                                        set_session_limits_cb,
                                        self);

  /* Update the cached copy */
  g_clear_pointer (&self->last_saved_limits, mct_session_limits_unref);
  self->last_saved_limits = g_steal_pointer (&new_limits);

  return G_SOURCE_REMOVE;
}

static void
screen_time_limit_row_notify_active_cb (MctTimePage *self)
{
  schedule_update_session_limits (self);
}

static void
bedtime_schedule_row_notify_active_cb (MctTimePage *self)
{
  schedule_update_session_limits (self);
}

static void
bedtime_row_notify_time_cb (CcTimeRow  *row,
                            GParamSpec *pspec,
                            gpointer    user_data)
{
  MctTimePage *self = MCT_TIME_PAGE (user_data);

  schedule_update_session_limits (self);
}

static void
daily_time_limit_row_notify_duration_cb (CcDurationRow *row,
                                         GParamSpec    *pspec,
                                         gpointer       user_data)
{
  MctTimePage *self = MCT_TIME_PAGE (user_data);

  schedule_update_session_limits (self);
}

static void
session_limits_changed_cb (MctManager *policy_manager,
                           uid_t       uid,
                           void       *user_data)
{
  MctTimePage *self = MCT_TIME_PAGE (user_data);

  if (uid != mct_user_get_uid (self->user))
    return;

  mct_manager_get_session_limits_async (self->policy_manager,
                                        mct_user_get_uid (self->user),
                                        MCT_MANAGER_GET_VALUE_FLAGS_NONE,
                                        self->cancellable,
                                        get_session_limits_cb,
                                        self);
}

static void
mct_time_page_get_property (GObject    *object,
                            guint       prop_id,
                            GValue     *value,
                            GParamSpec *pspec)
{
  MctTimePage *self = MCT_TIME_PAGE (object);

  switch ((MctTimePageProperty) prop_id)
    {
      case PROP_USER:
        g_value_set_object (value, self->user);
        break;

      case PROP_CONNECTION:
        g_value_set_object (value, self->connection);
        break;

      case PROP_POLICY_MANAGER:
        g_value_set_object (value, self->policy_manager);
        break;

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

static void
mct_time_page_set_property (GObject      *object,
                            guint         prop_id,
                            const GValue *value,
                            GParamSpec   *pspec)
{
  MctTimePage *self = MCT_TIME_PAGE (object);

  switch ((MctTimePageProperty) prop_id)
    {
      case PROP_USER:
        mct_time_page_set_user (self, g_value_get_object (value));
        break;

      case PROP_CONNECTION:
        /* Construct-only. May not be %NULL. */
        g_assert (self->connection == NULL);
        self->connection = g_value_dup_object (value);
        g_assert (self->connection != NULL);
        break;

      case PROP_POLICY_MANAGER:
        /* Construct only. */
        g_assert (self->policy_manager == NULL);
        self->policy_manager = g_value_dup_object (value);
        break;

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

static void
mct_time_page_constructed (GObject *object)
{
  MctTimePage *self = MCT_TIME_PAGE (object);

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

  /* Check our construct properties. */
  g_assert (self->connection != NULL);
  g_assert (MCT_IS_MANAGER (self->policy_manager));

  self->session_limits_changed_id =
    g_signal_connect (self->policy_manager,
                      "session-limits-changed",
                      G_CALLBACK (session_limits_changed_cb),
                      self);
}

static void
mct_time_page_dispose (GObject *object)
{
  MctTimePage *self = MCT_TIME_PAGE (object);

  /* Since GTK calls g_object_run_dispose(), dispose() may be called multiple
   * times. We definitely want to save any unsaved changes, but don’t need to
   * do it multiple times, and after the first g_object_run_dispose() call,
   * none of our child widgets are still around to extract data from anyway. */
  if (!self->flushed_on_dispose)
    flush_update_session_limits (self);
  self->flushed_on_dispose = TRUE;

  if (self->row != NULL)
    {
      adw_preferences_group_remove (self->preferences_group,
                                    GTK_WIDGET (self->row));
      self->row = NULL;
    }

  g_clear_object (&self->connection);

  g_cancellable_cancel (self->cancellable);
  g_clear_object (&self->cancellable);

  g_clear_signal_handler (&self->user_notify_id, self->user);
  g_clear_object (&self->user);

  g_clear_signal_handler (&self->session_limits_changed_id, self->policy_manager);
  g_clear_object (&self->policy_manager);

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

static void
mct_time_page_finalize (GObject *object)
{
  MctTimePage *self = MCT_TIME_PAGE (object);

  g_assert (self->update_session_limits_source_id == 0);

  g_clear_pointer (&self->limits, mct_session_limits_unref);
  g_clear_pointer (&self->last_saved_limits, mct_session_limits_unref);

  /* Hopefully we don’t have data loss. */
  g_assert (self->flushed_on_dispose);

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

static void
mct_time_page_class_init (MctTimePageClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  object_class->get_property = mct_time_page_get_property;
  object_class->set_property = mct_time_page_set_property;
  object_class->constructed = mct_time_page_constructed;
  object_class->dispose = mct_time_page_dispose;
  object_class->finalize = mct_time_page_finalize;

  /**
   * MctTimePage:user: (nullable)
   *
   * The currently selected user account.
   *
   * Since 0.14.0
   */
  properties[PROP_USER] =
      g_param_spec_object ("user",
                           "User",
                           "The currently selected user account.",
                           MCT_TYPE_USER,
                           G_PARAM_READWRITE |
                           G_PARAM_STATIC_STRINGS |
                           G_PARAM_EXPLICIT_NOTIFY);

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

  /**
   * MctTimePage:policy-manager:
   *
   * Policy manager to provider users’ parental controls policies.
   *
   * Since 0.14.0
   */
  properties[PROP_POLICY_MANAGER] =
      g_param_spec_object ("policy-manager", NULL, NULL,
                           MCT_TYPE_MANAGER,
                           G_PARAM_READWRITE |
                           G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties);

  g_type_ensure (CC_TYPE_DURATION_ROW);
  g_type_ensure (CC_TYPE_TIME_ROW);
  g_type_ensure (MCT_TYPE_SCREEN_TIME_STATISTICS_ROW);

  gtk_widget_class_set_template_from_resource (widget_class, "/org/freedesktop/MalcontentControl/ui/time-page.ui");

  gtk_widget_class_bind_template_child (widget_class, MctTimePage, time_window_title);
  gtk_widget_class_bind_template_child (widget_class, MctTimePage, preferences_group);
  gtk_widget_class_bind_template_child (widget_class, MctTimePage, preferences_page);
  gtk_widget_class_bind_template_child (widget_class, MctTimePage, screen_time_limit_row);
  gtk_widget_class_bind_template_child (widget_class, MctTimePage, bedtime_schedule_row);
  gtk_widget_class_bind_template_child (widget_class, MctTimePage, daily_time_limit_row);
  gtk_widget_class_bind_template_child (widget_class, MctTimePage, bedtime_row);

  gtk_widget_class_bind_template_callback (widget_class, screen_time_limit_row_notify_active_cb);
  gtk_widget_class_bind_template_callback (widget_class, bedtime_schedule_row_notify_active_cb);
  gtk_widget_class_bind_template_callback (widget_class, bedtime_row_notify_time_cb);
  gtk_widget_class_bind_template_callback (widget_class, daily_time_limit_row_notify_duration_cb);
}

static void
mct_time_page_init (MctTimePage *self)
{
  g_autoptr(GtkCssProvider) provider = NULL;

  gtk_widget_init_template (GTK_WIDGET (self));

  provider = gtk_css_provider_new ();
  gtk_css_provider_load_from_resource (provider, "/org/freedesktop/MalcontentControl/ui/wellbeing.css");
  gtk_style_context_add_provider_for_display (gdk_display_get_default (),
                                              GTK_STYLE_PROVIDER (provider),
                                              GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);

  self->cancellable = g_cancellable_new ();
}

/**
 * mct_time_page_new:
 * @policy_manager: (transfer none): policy manager for querying user parental
 *   controls policies
 * @connection: (transfer none): a D-Bus connection to use
 *
 * Create a new [class@Malcontent.TimePage] widget.
 *
 * Returns: (transfer full): a new time page
 * Since: 0.14.0
 */
MctTimePage *
mct_time_page_new (MctManager *policy_manager, GDBusConnection *connection)
{
  g_return_val_if_fail (MCT_IS_MANAGER (policy_manager), NULL);
  g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL);

  return g_object_new (MCT_TYPE_TIME_PAGE,
                       "policy-manager", policy_manager,
                       "connection", connection,
                       NULL);
}

/**
 * mct_time_page_get_user:
 * @self: a time page
 *
 * Get the currently selected user.
 *
 * Returns: (transfer none) (nullable): the currently selected user
 * Since: 0.14.0
 */
MctUser *
mct_time_page_get_user (MctTimePage *self)
{
  g_return_val_if_fail (MCT_IS_TIME_PAGE (self), NULL);

  return self->user;
}

static void
user_notify_cb (GObject    *object,
                GParamSpec *pspec,
                void       *user_data)
{
  MctUser *user = MCT_USER (object);
  MctTimePage *self = MCT_TIME_PAGE (user_data);

  g_autofree gchar *help_label = NULL;
  adw_window_title_set_subtitle (self->time_window_title,
                                     mct_user_get_display_name (user));

  /* Translators: Replace the link to commonsensemedia.org with some
   * localised guidance for parents/carers on how to set restrictions on
   * their child/caree in a responsible way which is in keeping with the
   * best practice and culture of the region. If no suitable localised
   * guidance exists, and if the default commonsensemedia.org link is not
   * suitable, please file an issue against malcontent so we can discuss
   * further!
   * https://gitlab.freedesktop.org/pwithnall/malcontent/-/issues/new
   */
  help_label = g_strdup_printf (_("It’s recommended that Screen Time "
                                  "limits and schedules are set as part of "
                                  "an ongoing conversation with %s. <a href='https://www.commonsensemedia.org/privacy-and-internet-safety'>"
                                  "Read guidance</a> on what to consider."),
                                mct_user_get_display_name (user));
  adw_preferences_page_set_description (self->preferences_page, help_label);
}

/**
 * mct_time_page_set_user:
 * @self: a time page
 * @user: (nullable): a user
 *
 * Set the currently selected user.
 *
 * Since: 0.14.0
 */
void
mct_time_page_set_user (MctTimePage *self,
                        MctUser     *user)
{
  g_return_if_fail (MCT_IS_TIME_PAGE (self));
  g_return_if_fail (user == NULL || MCT_IS_USER (user));

  g_autoptr(MctUser) old_user = NULL;
  uid_t uid;

  /* If we have pending unsaved changes from the previous user, force them to be
   * saved first. */
  flush_update_session_limits (self);

  old_user = (self->user != NULL) ? g_object_ref (self->user) : NULL;

  if (g_set_object (&self->user, user))
    {
      if (old_user != NULL)
        g_clear_signal_handler (&self->user_notify_id, old_user);

      if (user != NULL)
        {
          self->user_notify_id = g_signal_connect (user,
                                                   "notify",
                                                   G_CALLBACK (user_notify_cb),
                                                   self);
          user_notify_cb (G_OBJECT (user), NULL, self);

          mct_manager_get_session_limits_async (self->policy_manager,
                                                mct_user_get_uid (self->user),
                                                MCT_MANAGER_GET_VALUE_FLAGS_NONE,
                                                self->cancellable,
                                                get_session_limits_cb,
                                                self);

          if (self->row != NULL)
            {
              adw_preferences_group_remove (self->preferences_group,
                                            GTK_WIDGET (self->row));
              self->row = NULL;
            }

          uid = mct_user_get_uid (self->user);
          self->row = mct_screen_time_statistics_row_new (self->connection, uid);
          g_object_bind_property (self->daily_time_limit_row, "duration",
                                  self->row, "daily-limit",
                                  G_BINDING_SYNC_CREATE);

          adw_preferences_group_add (self->preferences_group,
                                     GTK_WIDGET (self->row));
        }

      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_USER]);
    }
}
