/* -*- 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>
#define GNOME_DESKTOP_USE_UNSTABLE_API
#include <libgnome-desktop/gnome-wall-clock.h>
#include <libmalcontent-ui/malcontent-ui.h>
#include <stdint.h>

#include "user-page.h"
#include "user-image.h"
#include "access-page.h"

#define CLOCK_SCHEMA "org.gnome.desktop.interface"
#define CLOCK_FORMAT_KEY "clock-format"

static void user_page_clock_changed_cb (GSettings *settings, char *key, void *user_data);
static void update_last_login_label (MctUserPage *self);
static void update_screen_time_level_bar (MctUserPage *self);

/* Copied from panels/wellbeing/cc-screen-time-statistics-row.c
 * in gnome-control-center */
static char *
format_hours_and_minutes (unsigned int minutes,
                          gboolean omit_minutes_if_zero)
{
  unsigned int hours = minutes / 60;
  minutes %= 60;

  /* Technically we should be formatting these units as per the SI Brochure,
   * table 8 and §5.4.3: with a 0+00A0 (non-breaking space) between the value
   * and unit; and using ‘min’ as the unit for minutes, not ‘m’.
   *
   * However, space is very restricted here, so we’re optimising for that.
   * Given that the whole panel is about screen *time*, hopefully the meaning of
   * the numbers should be obvious. */

  if (hours == 0 && minutes > 0)
    {
      /* Translators: This is a duration in minutes, for example ‘15m’ for 15 minutes.
       * Use whatever shortest unit label is used for minutes in your locale. */
      return g_strdup_printf (_("%um"), minutes);
    }
  else if (minutes == 0)
    {
      /* Translators: This is a duration in hours, for example ‘2h’ for 2 hours.
       * Use whatever shortest unit label is used for hours in your locale. */
      return g_strdup_printf (_("%uh"), hours);
    }
  else
    {
      /* Translators: This is a duration in hours and minutes, for example
       * ‘3h 15m’ for 3 hours and 15 minutes. Use whatever shortest unit label
       * is used for hours and minutes in your locale. */
      return g_strdup_printf (_("%uh %um"), hours, minutes);
    }
}

/**
 * MctUserPage:
 *
 * A widget which shows available parental controls for the selected user.
 *
 * [property@Malcontent.UserPage: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 _MctUserPage
{
  AdwNavigationPage parent;

  MctUserImage *user_image;
  GtkLabel *name_label;
  GtkLabel *date_label;
  GtkLabel *screen_time_label;
  GtkLabel *last_login_label;
  GtkLevelBar *screen_time_level_bar;
  AdwPreferencesRow *screen_time_row;
  GSettings *clock_settings; /* (owned) */
  GCancellable *cancellable; /* (owned) */
  unsigned long user_notify_id;
  unsigned int daily_limit_secs;
  gboolean daily_limit_enabled;
  uint64_t active_today_time_secs;

  unsigned long clock_changed_id;
  unsigned long session_limits_changed_id;
  unsigned int usage_changed_id;

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

G_DEFINE_TYPE (MctUserPage, mct_user_page, ADW_TYPE_NAVIGATION_PAGE)

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

static GParamSpec *properties[PROP_POLICY_MANAGER + 1];

static void
mct_user_page_get_property (GObject    *object,
                            guint       prop_id,
                            GValue     *value,
                            GParamSpec *pspec)
{
  MctUserPage *self = MCT_USER_PAGE (object);

  switch ((MctUserPageProperties) 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_user_page_set_property (GObject     *object,
                            guint        prop_id,
                            const GValue *value,
                            GParamSpec   *pspec)
{
  MctUserPage *self = MCT_USER_PAGE (object);

  switch ((MctUserPageProperties) prop_id)
    {
      case PROP_USER:
        mct_user_page_set_user (self, g_value_get_object (value), NULL);
        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. May not be %NULL. */
        g_assert (self->policy_manager == NULL);
        self->policy_manager = g_value_dup_object (value);
        g_assert (self->policy_manager != NULL);
        break;

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

static void
mct_user_page_constructed (GObject *object)
{
  MctUserPage *self = MCT_USER_PAGE (object);

  g_assert (self->connection != NULL);

  G_OBJECT_CLASS (mct_user_page_parent_class)->constructed (object);
}

static void
mct_user_page_dispose (GObject *object)
{
  MctUserPage *self = MCT_USER_PAGE (object);

  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->clock_changed_id, self->clock_settings);
  g_clear_signal_handler (&self->session_limits_changed_id, self->policy_manager);

  if (self->connection != NULL && self->usage_changed_id != 0)
    {
      g_dbus_connection_signal_unsubscribe (self->connection, self->usage_changed_id);
      self->usage_changed_id = 0;
    }

  g_clear_object (&self->clock_settings);
  g_clear_object (&self->connection);
  g_clear_object (&self->policy_manager);

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

static void
mct_user_page_class_init (MctUserPageClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  g_type_ensure (MCT_TYPE_USER_IMAGE);

  object_class->get_property = mct_user_page_get_property;
  object_class->set_property = mct_user_page_set_property;
  object_class->constructed = mct_user_page_constructed;
  object_class->dispose = mct_user_page_dispose;

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

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

  /**
   * MctUserPage:policy-manager: (not nullable)
   *
   * The policy manager providing the data for the widget.
   *
   * 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);

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

  gtk_widget_class_bind_template_child (widget_class, MctUserPage, user_image);
  gtk_widget_class_bind_template_child (widget_class, MctUserPage, name_label);
  gtk_widget_class_bind_template_child (widget_class, MctUserPage, date_label);
  gtk_widget_class_bind_template_child (widget_class, MctUserPage, screen_time_label);
  gtk_widget_class_bind_template_child (widget_class, MctUserPage, last_login_label);
  gtk_widget_class_bind_template_child (widget_class, MctUserPage, screen_time_level_bar);
  gtk_widget_class_bind_template_child (widget_class, MctUserPage, screen_time_row);
}

static void
mct_user_page_init (MctUserPage *self)
{
  gtk_widget_init_template (GTK_WIDGET (self));

  self->cancellable = g_cancellable_new ();

  self->clock_settings = g_settings_new (CLOCK_SCHEMA);

  self->clock_changed_id = g_signal_connect (self->clock_settings,
                                             "changed::" CLOCK_FORMAT_KEY,
                                             G_CALLBACK (user_page_clock_changed_cb),
                                             self);
}

static void
update_date_label (MctUserPage *self)
{
  g_autoptr (GDateTime) now_date_time = NULL;
  GDate today_date;
  char today_date_text[100] = { 0, };

  now_date_time = g_date_time_new_now_local ();
  g_date_set_time_t (&today_date, g_date_time_to_unix (now_date_time));
  g_date_strftime (today_date_text, sizeof (today_date_text), _("Today, %-d %B"), &today_date);
  gtk_label_set_label (self->date_label, today_date_text);
}

static void
update_screen_time_label (MctUserPage *self)
{
  g_autofree char *active_today_time_text = NULL;

  active_today_time_text = format_hours_and_minutes (self->active_today_time_secs / 60, FALSE);
  gtk_label_set_label (self->screen_time_label, active_today_time_text);
}

static void
update_screen_time_level_bar (MctUserPage *self)
{
  double max_value;

  max_value = self->daily_limit_secs;

  /* If the daily limit is not set, use 24 hours as
   * a maximum in the level bar. */
  if (self->daily_limit_secs == 0)
    {
      max_value = 24 * 60 * 60;
    }

  gtk_level_bar_set_value (self->screen_time_level_bar, CLAMP (self->active_today_time_secs, 1, max_value));
  gtk_level_bar_set_max_value (self->screen_time_level_bar, max_value);
}

static void
update_last_login_label (MctUserPage *self)
{
  GDesktopClockFormat clock_format;
  uint64_t login_time;
  g_autoptr (GDateTime) date_time = NULL;
  g_autofree char *last_login = NULL;
  g_autofree char *last_login_label = NULL;

  clock_format = g_settings_get_enum (self->clock_settings, CLOCK_FORMAT_KEY);
  login_time = mct_user_get_login_time (self->user);

  if (login_time == 0)
    {
      gtk_widget_set_visible (GTK_WIDGET (self->last_login_label), FALSE);
      return;
    }
  else
    {
      gtk_widget_set_visible (GTK_WIDGET (self->last_login_label), TRUE);
    }

  date_time = g_date_time_new_from_unix_local (login_time);

  if (clock_format == G_DESKTOP_CLOCK_FORMAT_12H)
    {
      /* Translators: This is the full date and time format used in 12-hour mode. */
      last_login = g_date_time_format (date_time, _("%-e %B %Y, %-l:%M %P"));
    }
  else
    {
      /* Translators: This is the full date and time format used in 24-hour mode. */
      last_login = g_date_time_format (date_time, _("%-e %B %Y, %-k:%M"));
    }

  last_login_label = g_strdup_printf (_("Last logged in: %s"), last_login);
  gtk_label_set_label (self->last_login_label, last_login_label);
}

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

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

static void
update_cached_usage (MctUserPage *self)
{
  g_dbus_connection_call (self->connection,
                          "org.freedesktop.MalcontentTimer1",
                          "/org/freedesktop/MalcontentTimer1",
                          "org.freedesktop.MalcontentTimer1.Parent",
                          "QueryUsage",
                          g_variant_new ("(uss)",
                                         mct_user_get_uid (self->user),
                                         "login-session",
                                         ""),
                          (const GVariantType *) "(a(tt))",
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          self->cancellable,
                          query_usage_cb,
                          self);
}

static void
update_cached_limits (MctUserPage *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);
}

static void
get_session_limits_cb (GObject      *object,
                       GAsyncResult *result,
                       void         *user_data)
{
  MctUserPage *self = MCT_USER_PAGE (user_data);
  g_autoptr (MctSessionLimits) limits = NULL;
  g_autoptr (GError) local_error = NULL;

  limits = mct_manager_get_session_limits_finish (self->policy_manager,
                                                  result,
                                                  &local_error);

  if (limits == NULL)
    {
      g_warning ("Error getting session limits: %s", local_error->message);
      return;
    }

  self->daily_limit_enabled = mct_session_limits_get_daily_limit (limits, &self->daily_limit_secs);

  update_screen_time_level_bar (self);
}

static void
policy_manager_session_limits_changed_cb (GObject *object,
                                          uid_t    uid,
                                          void    *user_data)
{
  MctUserPage *self = MCT_USER_PAGE (user_data);

  update_cached_limits (self);
}

static void
query_usage_cb (GObject      *object,
                GAsyncResult *result,
                void         *user_data)
{
  MctUserPage *self = MCT_USER_PAGE (user_data);
  g_autoptr (GDateTime) now_date_time = NULL;
  GDate today_date;
  g_autoptr (GVariant) result_variant = NULL;
  g_autoptr (GError) local_error = NULL;
  g_autoptr (GVariantIter) entries_iter = NULL;
  uint64_t start_wall_time_secs, end_wall_time_secs;

  now_date_time = g_date_time_new_now_local ();
  g_date_set_time_t (&today_date, g_date_time_to_unix (now_date_time));

  result_variant = g_dbus_connection_call_finish (self->connection,
                                                  result,
                                                  &local_error);

  if (result_variant == NULL)
    {
      g_warning ("Failed to get usage data for the level bar: %s", local_error->message);
      return;
    }

  g_variant_get (result_variant, "(a(tt))", &entries_iter);

  self->active_today_time_secs = 0;

  while (g_variant_iter_loop (entries_iter, "(tt)", &start_wall_time_secs, &end_wall_time_secs))
    {
      GDate start_date, end_date;

      g_date_set_time_t (&start_date, start_wall_time_secs);
      g_date_set_time_t (&end_date, end_wall_time_secs);

      if (g_date_compare (&end_date, &today_date) != 0)
        {
          continue;
        }

      /* If the recorded usage entry began on an earlier day than today,
       * clamp it to the beginning of today */
      if (g_date_compare (&start_date, &end_date) != 0)
        {
          g_autoptr (GDateTime) date_time = NULL;
          g_autoptr (GTimeZone) time_zone = NULL;

          time_zone = g_time_zone_new_local ();
          date_time = g_date_time_new (time_zone,
                                       g_date_get_year (&end_date),
                                       g_date_get_month (&end_date),
                                       g_date_get_day (&end_date),
                                       0, 0, 0.0);
          start_wall_time_secs = g_date_time_to_unix (date_time);
        }

      self->active_today_time_secs += end_wall_time_secs - start_wall_time_secs;
    }

  gtk_widget_set_visible (GTK_WIDGET (self->screen_time_row),
                          self->active_today_time_secs != 0);

  update_date_label (self);
  update_screen_time_level_bar (self);
  update_screen_time_label (self);
}

static void
usage_changed_cb (GDBusConnection *connection,
                  const char      *sender_name,
                  const char      *object_path,
                  const char      *interface_name,
                  const char      *signal_name,
                  GVariant        *parameters,
                  void            *user_data)
{
  MctUserPage *self = MCT_USER_PAGE (user_data);

  update_cached_usage (self);
}

static void
user_page_clock_changed_cb (GSettings *settings,
                            char      *key,
                            void      *user_data)
{
  MctUserPage *self = MCT_USER_PAGE (user_data);

  update_last_login_label (self);
}

/**
 * mct_user_page_new:
 * @connection: (transfer none): a D-Bus connection to use
 * @policy-manager: (transfer none): a policy manager to use
 *
 * Create a new [class@Malcontent.UserPage] widget.
 *
 * Returns: (transfer full): a new user page
 * Since: 0.14.0
 */
MctUserPage *
mct_user_page_new (GDBusConnection *connection,
                   MctManager      *policy_manager)
{
  return g_object_new (MCT_TYPE_USER_PAGE,
                       "connection", connection,
                       "policy-manager", policy_manager,
                       NULL);
}

/**
 * mct_user_page_get_user:
 * @self: a user page
 *
 * Get the currently selected user.
 *
 * Returns: (transfer none) (nullable): the currently selected user
 * Since: 0.14.0
 */
MctUser *
mct_user_page_get_user (MctUserPage *self)
{
  g_return_val_if_fail (MCT_IS_USER_PAGE (self), NULL);

  return self->user;
}

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

  gtk_label_set_label (self->name_label, mct_user_get_display_name (user));
  update_last_login_label (self);
}

/**
 * mct_user_page_set_user:
 * @self: a user page
 * @user: (nullable): a user
 * @permission: (nullable) (transfer none): the [class@Gio.Permission]
 *   indicating whether the current user has permission to view or change
 *   parental controls, or `NULL` if permission is not allowed or is unknown
 *
 * Set the currently selected user
 *
 * Since: 0.14.0
 */
void
mct_user_page_set_user (MctUserPage *self,
                        MctUser     *user,
                        GPermission *permission)
{
  g_return_if_fail (MCT_IS_USER_PAGE (self));
  g_return_if_fail (MCT_IS_USER (user));
  g_return_if_fail (permission == NULL || G_IS_PERMISSION (permission));

  if (g_set_object (&self->user, user))
    {
      if (user != NULL)
        {
          mct_user_image_set_user (self->user_image, user);
          mct_user_image_set_size (self->user_image, 128);
          gtk_widget_set_margin_top (GTK_WIDGET (self->user_image), 24);
          gtk_widget_set_margin_bottom (GTK_WIDGET (self->user_image), 18);

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

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

          update_cached_usage (self);
          self->usage_changed_id =
            g_dbus_connection_signal_subscribe (self->connection,
                                                "org.freedesktop.MalcontentTimer1",
                                                "org.freedesktop.MalcontentTimer1.Parent",
                                                "UsageChanged",
                                                "/org/freedesktop/MalcontentTimer1",
                                                NULL,
                                                G_DBUS_SIGNAL_FLAGS_NONE,
                                                usage_changed_cb,
                                                self,
                                                NULL);
        }

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