/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
 * Copyright © 2019 Endless Mobile, Inc.
 * 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 <withnall@endlessm.com>
 */

#include "config.h"

#include <glib.h>
#include <glib-object.h>
#include <glib/gi18n-lib.h>
#include <gio/gio.h>
#include <libmalcontent/manager.h>
#include <libmalcontent/session-limits.h>

#include "libmalcontent/session-limits-private.h"


/* struct _MctSessionLimits is defined in session-limits-private.h */

G_DEFINE_BOXED_TYPE (MctSessionLimits, mct_session_limits,
                     mct_session_limits_ref, mct_session_limits_unref)

/**
 * mct_session_limits_ref:
 * @limits: (transfer none): a [struct@Malcontent.SessionLimits]
 *
 * Increment the reference count of @limits, and return the same pointer to it.
 *
 * Returns: (transfer full): the same pointer as @limits
 * Since: 0.5.0
 */
MctSessionLimits *
mct_session_limits_ref (MctSessionLimits *limits)
{
  g_return_val_if_fail (limits != NULL, NULL);
  g_return_val_if_fail (limits->ref_count >= 1, NULL);
  g_return_val_if_fail (limits->ref_count <= G_MAXINT - 1, NULL);

  limits->ref_count++;
  return limits;
}

/**
 * mct_session_limits_unref:
 * @limits: (transfer full): a [struct@Malcontent.SessionLimits]
 *
 * Decrement the reference count of @limits. If the reference count reaches
 * zero, free the @limits and all its resources.
 *
 * Since: 0.5.0
 */
void
mct_session_limits_unref (MctSessionLimits *limits)
{
  g_return_if_fail (limits != NULL);
  g_return_if_fail (limits->ref_count >= 1);

  limits->ref_count--;

  if (limits->ref_count <= 0)
    {
      g_free (limits);
    }
}

/**
 * mct_session_limits_get_user_id:
 * @limits: a [struct@Malcontent.SessionLimits]
 *
 * Get the user ID of the user this [struct@Malcontent.SessionLimits] is for.
 *
 * Returns: user ID of the relevant user, or `(uid_t) -1` if unknown
 * Since: 0.5.0
 */
uid_t
mct_session_limits_get_user_id (MctSessionLimits *limits)
{
  g_return_val_if_fail (limits != NULL, (uid_t) -1);
  g_return_val_if_fail (limits->ref_count >= 1, (uid_t) -1);

  return limits->user_id;
}

/**
 * mct_session_limits_is_enabled:
 * @limits: a [struct@Malcontent.SessionLimits]
 *
 * Check whether any session limits are enabled and are going to impose at least
 * one restriction on the user.
 *
 * This gives a high level view of whether session limit parental controls are
 * ‘enabled’ for the given user.
 *
 * This function is equivalent to the value returned by the
 * `time_limit_enabled_out` argument of
 * [method@Malcontent.SessionLimits.check_time_remaining].
 *
 * Returns: true if the session limits object contains at least one restrictive
 *   session limit, false if there are no limits in place
 * Since: 0.7.0
 */
gboolean
mct_session_limits_is_enabled (MctSessionLimits *limits)
{
  g_return_val_if_fail (limits != NULL, FALSE);
  g_return_val_if_fail (limits->ref_count >= 1, FALSE);

  return (limits->limit_type != MCT_SESSION_LIMITS_TYPE_NONE);
}

/**
 * mct_session_limits_get_daily_schedule:
 * @limits: a [struct@Malcontent.SessionLimits]
 * @out_start_time_secs: (out) (optional): return location for the earliest
 *   allowable session start time for the user, in seconds since midnight
 * @out_end_time_secs: (out) (optional): return location for the latest
 *   allowable session end time for the user, in seconds since midnight
 *
 * Get the daily schedule session limits, if set.
 *
 * If set, sessions are allowed between the given start and end time every day.
 * The times are given as offsets from the start of the day, in seconds.
 *
 * @out_end_time_secs is guaranteed to be greater than @out_start_time_secs, if
 * they are set. @out_end_time_secs is guaranteed to be at most `24 * 60 * 60`
 * if set.
 *
 * Returns: true if a daily schedule is set, false otherwise
 * Since: 0.14.0
 */
gboolean
mct_session_limits_get_daily_schedule (MctSessionLimits *limits,
                                       unsigned int     *out_start_time_secs,
                                       unsigned int     *out_end_time_secs)
{
  g_return_val_if_fail (limits != NULL, FALSE);
  g_return_val_if_fail (limits->ref_count >= 1, FALSE);

  if (out_start_time_secs != NULL)
    *out_start_time_secs = (limits->set_limit_type & MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE) ? limits->daily_start_time : 0;
  if (out_end_time_secs != NULL)
    *out_end_time_secs = (limits->set_limit_type & MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE) ? limits->daily_end_time : 0;

  return (limits->limit_type & MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE);
}

/**
 * mct_session_limits_get_daily_limit:
 * @limits: a [struct@Malcontent.SessionLimits]
 * @out_daily_limit_secs: (out) (optional): return location for the maximum
 *   amount of active session time allowed per day for the user, in seconds
 *
 * Get the daily limit, if set.
 *
 * If set, sessions are allowed to be up to the given limit in length every day.
 *
 * @out_daily_limit_secs is guaranteed to be at most `24 * 60 * 60` if set.
 *
 * Returns: true if a daily limit is set, false otherwise
 * Since: 0.14.0
 */
gboolean
mct_session_limits_get_daily_limit (MctSessionLimits *limits,
                                    unsigned int     *out_daily_limit_secs)
{
  g_return_val_if_fail (limits != NULL, FALSE);
  g_return_val_if_fail (limits->ref_count >= 1, FALSE);

  if (out_daily_limit_secs != NULL)
    *out_daily_limit_secs = (limits->set_limit_type & MCT_SESSION_LIMITS_TYPE_DAILY_LIMIT) ? limits->daily_limit_secs : 0;

  return (limits->limit_type & MCT_SESSION_LIMITS_TYPE_DAILY_LIMIT);
}

/**
 * mct_session_limits_get_active_extension:
 * @limits: a [struct@Malcontent.SessionLimits]
 * @now_dt: current time in the user’s timezone, typically queried using
 *   `g_date_time_new_now_local()`
 * @out_start_time_secs: (out) (optional): return location for the start time of
 *   the extension, in seconds since the Unix epoch; or zero if there is no
 *   active extension
 * @out_duration_secs: (out) (optional): return location for the duration of the
 *   extension, in seconds; or zero if there is no active extension
 *
 * Check whether there is an active screen time limit extension, and get its
 * bounds.
 *
 * If a screen time limit extension has been set on @limits using
 * [method@Malcontent.SessionLimitsBuilder.set_active_extension], and @limits
 * has at least one limit enabled (see
 * [method@Malcontent.SessionLimits.is_enabled]), and the bounds of the
 * extension contain @now_dt (i.e. the extension isn’t entirely in the past or
 * the future), then the out arguments will be set to its bounds and true will
 * be returned.
 *
 * Otherwise, there is no screen time limit extension active, so false will be
 * returned and the out arguments will be set to zero.
 *
 * Returns: true if @limits has a screen time limit extension active; false
 *   otherwise
 * Since: 0.14.0
 */
gboolean
mct_session_limits_get_active_extension (MctSessionLimits *limits,
                                         GDateTime        *now_dt,
                                         uint64_t         *out_start_time_secs,
                                         unsigned int     *out_duration_secs)
{
  gboolean extension_is_active;
  uint64_t now_secs;
  uint64_t start_time_secs;
  unsigned int duration_secs;

  g_return_val_if_fail (limits != NULL, FALSE);
  g_return_val_if_fail (limits->ref_count >= 1, FALSE);
  g_return_val_if_fail (now_dt != NULL, FALSE);

  /* We check for this being very far in the future in the `if` block below. */
  now_secs = g_date_time_to_unix (now_dt);

  if (limits->set_limit_type == MCT_SESSION_LIMITS_TYPE_NONE ||
      g_date_time_to_unix (now_dt) < 0 ||
      limits->active_extension_duration_secs == 0 ||
      limits->active_extension_start_time_secs > now_secs ||
      limits->active_extension_start_time_secs + limits->active_extension_duration_secs <= now_secs)
    {
      extension_is_active = FALSE;
      start_time_secs = 0;
      duration_secs = 0;
    }
  else
    {
      extension_is_active = TRUE;
      start_time_secs = limits->active_extension_start_time_secs;
      duration_secs = limits->active_extension_duration_secs;
    }

  if (out_start_time_secs != NULL)
    *out_start_time_secs = start_time_secs;
  if (out_duration_secs != NULL)
    *out_duration_secs = duration_secs;

  return extension_is_active;
}

/**
 * mct_session_limits_check_time_remaining:
 * @limits: a [struct@Malcontent.SessionLimits]
 * @now_dt: current time in the user’s timezone, typically queried using
 *   `g_date_time_new_now_local()`
 * @active_session_time_today_secs: total time the user has spent in an active
 *   session so far today, in seconds
 * @time_remaining_secs_out: (out) (optional): return location for the number
 *   of seconds remaining before the user’s session has to end, if limits are
 *   in force
 * @time_limit_enabled_out: (out) (optional): return location for whether time
 *   limits are enabled for this user
 * @extension_active_out: (out) (optional): return location for whether @limits
 *   has a screen time limit extension active (see
 *   [method@Malcontent.SessionLimits.get_active_extension])
 *
 * Check whether the user has time remaining in which they are allowed to use
 * the computer.
 *
 * This assumes that @now_dt is the current time and
 * @active_session_time_today_secs is the total amount of time the user has
 * spent in an active session up to that point today, and applies the
 * session limit policy from @limits to them.
 *
 * This will return whether the user is allowed to use the computer now; further
 * information about the policy and remaining time is provided in
 * @time_remaining_secs_out and @time_limit_enabled_out.
 *
 * Returns: true if the user this @limits corresponds to is allowed to be in
 *   an active session at the given time; false otherwise
 * Since: 0.14.0
 */
gboolean
mct_session_limits_check_time_remaining (MctSessionLimits *limits,
                                         GDateTime        *now_dt,
                                         uint64_t          active_session_time_today_secs,
                                         guint64          *time_remaining_secs_out,
                                         gboolean         *time_limit_enabled_out,
                                         gboolean         *extension_active_out)
{
  guint64 time_remaining_secs;
  gboolean time_limit_enabled = FALSE;
  gboolean user_allowed_now = TRUE;
  uint64_t now_secs;
  guint64 now_time_of_day_secs;
  gboolean extension_active;
  uint64_t active_extension_start_time_secs, active_extension_end_time_secs;
  uint64_t active_extension_start_time_of_day_secs, active_extension_end_time_of_day_secs;
  unsigned int active_extension_duration_secs;
  g_autoptr(GDateTime) start_of_today = NULL, start_of_tomorrow = NULL;
  uint64_t start_of_today_secs, start_of_tomorrow_secs;

  g_return_val_if_fail (limits != NULL, FALSE);
  g_return_val_if_fail (limits->ref_count >= 1, FALSE);
  g_return_val_if_fail (now_dt != NULL, FALSE);

  /* Helper calculations. If we end up with a @now_dt before the UNIX epoch,
   * the caller has provided a date very far in the future which we don’t yet
   * support.
   *
   * This needs to be in the user’s local timezone, because `DAILY_SCHEDULE`
   * limits are essentially in the local timezone by virtue of being wall clock
   * times. */
  if (g_date_time_to_unix (now_dt) < 0)
    {
      time_remaining_secs = 0;
      time_limit_enabled = TRUE;
      user_allowed_now = FALSE;
      extension_active = FALSE;
      goto out;
    }

  extension_active = mct_session_limits_get_active_extension (limits,
                                                              now_dt,
                                                              &active_extension_start_time_secs,
                                                              &active_extension_duration_secs);
  if (extension_active)
    g_debug ("%s: Extension active from %" G_GUINT64_FORMAT " for %u seconds",
             G_STRFUNC, active_extension_start_time_secs, active_extension_duration_secs);
  else
    g_debug ("%s: No extension active", G_STRFUNC);

  /* Work out some times */
  now_time_of_day_secs = ((g_date_time_get_hour (now_dt) * 60 +
                           g_date_time_get_minute (now_dt)) * 60 +
                           g_date_time_get_second (now_dt));
  start_of_today = g_date_time_new_local (g_date_time_get_year (now_dt),
                                          g_date_time_get_month (now_dt),
                                          g_date_time_get_day_of_month (now_dt),
                                          0, 0, 0);
  start_of_tomorrow = g_date_time_add_days (start_of_today, 1);
  g_assert (start_of_today != NULL);
  g_assert (start_of_tomorrow != NULL);
  start_of_today_secs = g_date_time_to_unix (start_of_today);
  start_of_tomorrow_secs = g_date_time_to_unix (start_of_tomorrow);
  active_extension_end_time_secs = active_extension_start_time_secs + active_extension_duration_secs;
  active_extension_start_time_of_day_secs = active_extension_start_time_secs - start_of_today_secs;
  active_extension_end_time_of_day_secs = active_extension_end_time_secs - start_of_today_secs;
  now_secs = g_date_time_to_unix (now_dt);
  time_remaining_secs = start_of_tomorrow_secs - now_secs;

  g_debug ("%s:\n"
           "\tstart_of_today_secs %" G_GUINT64_FORMAT "\n"
           "\tstart_of_tomorrow_secs %" G_GUINT64_FORMAT "\n"
           "\tactive_extension_end_time_secs %" G_GUINT64_FORMAT "\n"
           "\tactive_extension_start_time_of_day_secs %" G_GUINT64_FORMAT "\n"
           "\tactive_extension_end_time_of_day_secs %" G_GUINT64_FORMAT "\n"
           "\tnow_secs %" G_GUINT64_FORMAT "\n"
           "\ttime_remaining_secs %" G_GUINT64_FORMAT,
           G_STRFUNC,
           start_of_today_secs, start_of_tomorrow_secs, active_extension_end_time_secs,
           active_extension_start_time_of_day_secs, active_extension_end_time_of_day_secs,
           now_secs, time_remaining_secs);

  /* This is guaranteed by mct_session_limits_get_active_extension() */
  if (extension_active)
    {
      g_assert (active_extension_start_time_secs <= now_secs);
      g_assert (active_extension_end_time_secs > now_secs);
    }

  /* Work out the limits. */
  if (limits->limit_type & MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE)
    {
      uint64_t effective_daily_start_time = limits->daily_start_time;
      uint64_t effective_daily_end_time = limits->daily_end_time;

      /* Apply the time limit extension, if applicable */
      if (extension_active &&
          active_extension_start_time_of_day_secs < effective_daily_start_time)
        effective_daily_start_time = active_extension_start_time_of_day_secs;
      if (extension_active &&
          active_extension_end_time_of_day_secs > effective_daily_end_time)
        effective_daily_end_time = active_extension_end_time_of_day_secs;

      user_allowed_now = user_allowed_now &&
                         (now_time_of_day_secs >= effective_daily_start_time &&
                          now_time_of_day_secs < effective_daily_end_time);
      time_remaining_secs = user_allowed_now ? MIN (time_remaining_secs, effective_daily_end_time - now_time_of_day_secs) : 0;
      time_limit_enabled = TRUE;

      g_debug ("%s: Daily schedule limit allowed in %" G_GUINT64_FORMAT "–%"
               G_GUINT64_FORMAT " (configured %u–%u; now is %"
               G_GUINT64_FORMAT "); %" G_GUINT64_FORMAT " seconds remaining",
               G_STRFUNC, effective_daily_start_time, effective_daily_end_time,
               limits->daily_start_time, limits->daily_end_time,
               now_time_of_day_secs, time_remaining_secs);
    }

  if (limits->limit_type & MCT_SESSION_LIMITS_TYPE_DAILY_LIMIT)
    {
      unsigned int effective_daily_limit_secs = limits->daily_limit_secs;

      /* Apply the time limit extension, if applicable */
      if (extension_active &&
          (effective_daily_limit_secs < active_session_time_today_secs ||
           active_extension_end_time_secs - now_secs > effective_daily_limit_secs - active_session_time_today_secs))
        effective_daily_limit_secs = active_extension_end_time_secs - now_secs + active_session_time_today_secs;

      user_allowed_now = (user_allowed_now &&
                          active_session_time_today_secs < effective_daily_limit_secs);
      time_remaining_secs = user_allowed_now ? MIN (time_remaining_secs, effective_daily_limit_secs - active_session_time_today_secs) : 0;
      time_limit_enabled = TRUE;

      g_debug ("%s: Daily limit allowed up to %u (configured %u; currently used %"
               G_GUINT64_FORMAT "); %" G_GUINT64_FORMAT " seconds remaining",
               G_STRFUNC, effective_daily_limit_secs, limits->daily_limit_secs,
               active_session_time_today_secs, time_remaining_secs);
    }

  if (limits->limit_type == MCT_SESSION_LIMITS_TYPE_NONE)
    {
      user_allowed_now = TRUE;
      time_remaining_secs = G_MAXUINT64;
      time_limit_enabled = FALSE;

      g_debug ("%s: No limit enabled", G_STRFUNC);
    }

out:
  /* Postconditions. */
  g_assert (!user_allowed_now || time_remaining_secs > 0);
  g_assert (user_allowed_now || time_remaining_secs == 0);
  g_assert (time_limit_enabled || time_remaining_secs == G_MAXUINT64);
  g_assert (time_limit_enabled || !extension_active);

  /* Output. */
  if (time_remaining_secs_out != NULL)
    *time_remaining_secs_out = time_remaining_secs;
  if (time_limit_enabled_out != NULL)
    *time_limit_enabled_out = time_limit_enabled;
  if (extension_active_out != NULL)
    *extension_active_out = extension_active;

  return user_allowed_now;
}

/**
 * mct_session_limits_serialize:
 * @limits: a [struct@Malcontent.SessionLimits]
 *
 * Build a [struct@GLib.Variant] which contains the session limits from @limits,
 * in an opaque variant format.
 *
 * This format may change in future, but
 * [func@Malcontent.SessionLimits.deserialize] is guaranteed to always be able
 * to load any variant produced by the current or any previous version of
 * [method@Malcontent.SessionLimits.serialize].
 *
 * Returns: (transfer floating): a new, floating [struct@GLib.Variant]
 *   containing the session limits
 * Since: 0.7.0
 */
GVariant *
mct_session_limits_serialize (MctSessionLimits *limits)
{
  g_auto(GVariantBuilder) builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("a{sv}"));

  g_return_val_if_fail (limits != NULL, NULL);
  g_return_val_if_fail (limits->ref_count >= 1, NULL);

  /* The serialisation format is exactly the
   * `com.endlessm.ParentalControls.SessionLimits` D-Bus interface. */
  if (limits->set_limit_type & MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE)
    g_variant_builder_add (&builder, "{sv}", "DailySchedule",
                           g_variant_new ("(uu)",
                                          limits->daily_start_time,
                                          limits->daily_end_time));

  if (limits->set_limit_type & MCT_SESSION_LIMITS_TYPE_DAILY_LIMIT)
    g_variant_builder_add (&builder, "{sv}", "DailyLimit",
                           g_variant_new ("u", limits->daily_limit_secs));

  if (limits->set_limit_type != MCT_SESSION_LIMITS_TYPE_NONE)
    g_variant_builder_add (&builder, "{sv}", "ActiveExtension",
                           g_variant_new ("(tu)",
                                          limits->active_extension_start_time_secs,
                                          limits->active_extension_duration_secs));

  g_variant_builder_add (&builder, "{sv}", "LimitType",
                         g_variant_new_uint32 (limits->limit_type));

  return g_variant_builder_end (&builder);
}

/**
 * mct_session_limits_deserialize:
 * @variant: a serialized session limits variant
 * @user_id: the ID of the user the session limits relate to
 * @error: return location for a [type@GLib.Error], or `NULL`
 *
 * Deserialize a set of session limits previously serialized with
 * [method@Malcontent.SessionLimits.serialize].
 *
 * This function guarantees to be able to deserialize any serialized form from
 * this version or older versions of libmalcontent.
 *
 * If deserialization fails, [error@Malcontent.ManagerError.INVALID_DATA] will
 * be returned.
 *
 * Returns: (transfer full): deserialized session limits
 * Since: 0.7.0
 */
MctSessionLimits *
mct_session_limits_deserialize (GVariant  *variant,
                                uid_t      user_id,
                                GError   **error)
{
  g_autoptr(MctSessionLimits) session_limits = NULL;
  guint32 limit_type;
  MctSessionLimitsType set_limit_type = MCT_SESSION_LIMITS_TYPE_NONE;
  guint32 daily_start_time, daily_end_time;
  uint32_t daily_limit_secs;
  uint64_t active_extension_start_time_secs;
  unsigned int active_extension_duration_secs;

  g_return_val_if_fail (variant != NULL, NULL);
  g_return_val_if_fail (error == NULL || *error == NULL, NULL);

  /* Check the overall type. */
  if (!g_variant_is_of_type (variant, G_VARIANT_TYPE ("a{sv}")))
    {
      g_set_error (error, MCT_MANAGER_ERROR,
                   MCT_MANAGER_ERROR_INVALID_DATA,
                   _("Session limit for user %u was in an unrecognized format"),
                   (guint) user_id);
      return NULL;
    }

  /* Extract the properties we care about. The default values here should be
   * kept in sync with those in the `com.endlessm.ParentalControls.SessionLimits`
   * D-Bus interface. */
  if (!g_variant_lookup (variant, "LimitType", "u",
                         &limit_type))
    {
      /* Default value. */
      limit_type = MCT_SESSION_LIMITS_TYPE_NONE;
    }

  /* Check that the limit type is something we support. */
  G_STATIC_ASSERT (sizeof (limit_type) >= sizeof (MctSessionLimitsType));

  if (((guint) limit_type & ~MCT_SESSION_LIMITS_TYPE_MASK) != 0)
    {
      g_set_error (error, MCT_MANAGER_ERROR,
                   MCT_MANAGER_ERROR_INVALID_DATA,
                   _("Session limit for user %u has an unrecognized type ‘%u’"),
                   (guint) user_id, limit_type);
      return NULL;
    }

  /* Daily schedule */
  if (!g_variant_lookup (variant, "DailySchedule", "(uu)",
                         &daily_start_time, &daily_end_time))
    {
      /* Default value. */
      daily_start_time = 0;
      daily_end_time = 24 * 60 * 60;
    }
  else
    {
      set_limit_type |= MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE;
    }

  if (daily_start_time >= daily_end_time ||
      daily_end_time > 24 * 60 * 60)
    {
      g_set_error (error, MCT_MANAGER_ERROR,
                   MCT_MANAGER_ERROR_INVALID_DATA,
                   _("Session limit for user %u has invalid daily schedule %u–%u"),
                   (guint) user_id, daily_start_time, daily_end_time);
      return NULL;
    }

  /* Daily limit */
  if (!g_variant_lookup (variant, "DailyLimit", "u", &daily_limit_secs))
    {
      /* Default value. */
      daily_limit_secs = 24 * 60 * 60;
    }
  else
    {
      set_limit_type |= MCT_SESSION_LIMITS_TYPE_DAILY_LIMIT;
    }

  if (daily_limit_secs > 24 * 60 * 60)
    {
      g_set_error (error, MCT_MANAGER_ERROR,
                   MCT_MANAGER_ERROR_INVALID_DATA,
                   _("Session limit for user %u has invalid daily limit %u"),
                   (guint) user_id, daily_limit_secs);
      return NULL;
    }

  /* Active extension */
  if (!g_variant_lookup (variant, "ActiveExtension", "(tu)",
                         &active_extension_start_time_secs,
                         &active_extension_duration_secs))
    {
      /* Default value. */
      active_extension_start_time_secs = 0;
      active_extension_duration_secs = 0;
    }

  if (active_extension_duration_secs > UINT64_MAX - active_extension_start_time_secs)
    {
      g_autofree char *start_time_str = g_strdup_printf ("%" G_GUINT64_FORMAT, active_extension_start_time_secs);
      g_set_error (error, MCT_MANAGER_ERROR,
                   MCT_MANAGER_ERROR_INVALID_DATA,
                   _("Session limit for user %u has invalid active extension %s+%u"),
                   (guint) user_id, start_time_str,
                   active_extension_duration_secs);
      return NULL;
    }

  /* Ensure @set_limit_type is equal to, or a superset of, @limit_type. */
  set_limit_type |= limit_type;

  /* Success. Create an `MctSessionLimits` object to contain the results. */
  session_limits = g_new0 (MctSessionLimits, 1);
  session_limits->ref_count = 1;
  session_limits->user_id = user_id;
  session_limits->limit_type = limit_type;
  session_limits->set_limit_type = set_limit_type;
  session_limits->daily_start_time = daily_start_time;
  session_limits->daily_end_time = daily_end_time;
  session_limits->daily_limit_secs = daily_limit_secs;
  session_limits->active_extension_start_time_secs = active_extension_start_time_secs;
  session_limits->active_extension_duration_secs = active_extension_duration_secs;

  return g_steal_pointer (&session_limits);
}

/**
 * mct_session_limits_equal:
 * @a: (not nullable): a session limits configuration
 * @b: (not nullable): a session limits configuration
 *
 * Check whether session limits configurations @a and @b are equal.
 *
 * Returns: true if @a and @b are equal, false otherwise
 * Since: 0.14.0
 */
gboolean
mct_session_limits_equal (MctSessionLimits *a,
                          MctSessionLimits *b)
{
  g_return_val_if_fail (a != NULL, FALSE);
  g_return_val_if_fail (a->ref_count >= 1, FALSE);
  g_return_val_if_fail (b != NULL, FALSE);
  g_return_val_if_fail (b->ref_count >= 1, FALSE);

  return (a->user_id == b->user_id &&
          a->limit_type == b->limit_type &&
          a->set_limit_type == b->set_limit_type &&
          a->daily_start_time == b->daily_start_time &&
          a->daily_end_time == b->daily_end_time &&
          a->daily_limit_secs == b->daily_limit_secs &&
          a->active_extension_start_time_secs == b->active_extension_start_time_secs &&
          a->active_extension_duration_secs == b->active_extension_duration_secs);
}

/*
 * Actual implementation of `MctSessionLimitsBuilder`.
 *
 * All members are `NULL` if un-initialised, cleared, or ended.
 */
typedef struct
{
  MctSessionLimitsType limit_type;
  MctSessionLimitsType set_limit_type;

  struct
    {
      guint start_time;  /* seconds since midnight */
      guint end_time;  /* seconds since midnight */
    } daily_schedule;

  unsigned int daily_limit_secs;

  uint64_t active_extension_start_time_secs;
  unsigned int active_extension_duration_secs;

  /*< private >*/
  gpointer padding[9];
} MctSessionLimitsBuilderReal;

G_STATIC_ASSERT (sizeof (MctSessionLimitsBuilderReal) ==
                 sizeof (MctSessionLimitsBuilder));
G_STATIC_ASSERT (__alignof__ (MctSessionLimitsBuilderReal) ==
                 __alignof__ (MctSessionLimitsBuilder));

G_DEFINE_BOXED_TYPE (MctSessionLimitsBuilder, mct_session_limits_builder,
                     mct_session_limits_builder_copy, mct_session_limits_builder_free)

/**
 * mct_session_limits_builder_init:
 * @builder: an uninitialised [struct@Malcontent.SessionLimitsBuilder]
 *
 * Initialise the given @builder so it can be used to construct a new
 * [struct@Malcontent.SessionLimits].
 *
 * @builder must have been allocated on the stack, and must not already be
 * initialised.
 *
 * Construct the [struct@Malcontent.SessionLimits] by calling methods on
 * @builder, followed by [method@Malcontent.SessionLimitsBuilder.end]. To abort
 * construction, use [method@Malcontent.SessionLimitsBuilder.clear].
 *
 * Since: 0.5.0
 */
void
mct_session_limits_builder_init (MctSessionLimitsBuilder *builder)
{
  MctSessionLimitsBuilder local_builder = MCT_SESSION_LIMITS_BUILDER_INIT ();
  MctSessionLimitsBuilderReal *_builder = (MctSessionLimitsBuilderReal *) builder;

  g_return_if_fail (_builder != NULL);
  g_return_if_fail (_builder->limit_type == MCT_SESSION_LIMITS_TYPE_NONE);
  g_return_if_fail (_builder->set_limit_type == MCT_SESSION_LIMITS_TYPE_NONE);

  memcpy (builder, &local_builder, sizeof (local_builder));
}

/**
 * mct_session_limits_builder_clear:
 * @builder: a [struct@Malcontent.SessionLimitsBuilder]
 *
 * Clear @builder, freeing any internal state in it.
 *
 * This will not free the top-level storage for @builder itself, which is
 * assumed to be allocated on the stack.
 *
 * If called on an already-cleared [struct@Malcontent.SessionLimitsBuilder],
 * this function is idempotent.
 *
 * Since: 0.5.0
 */
void
mct_session_limits_builder_clear (MctSessionLimitsBuilder *builder)
{
  MctSessionLimitsBuilderReal *_builder = (MctSessionLimitsBuilderReal *) builder;

  g_return_if_fail (_builder != NULL);

  /* Nothing to free here for now. */
  _builder->limit_type = MCT_SESSION_LIMITS_TYPE_NONE;
  _builder->set_limit_type = MCT_SESSION_LIMITS_TYPE_NONE;
}

/**
 * mct_session_limits_builder_new:
 *
 * Construct a new [struct@Malcontent.SessionLimitsBuilder] on the heap.
 *
 * This is intended for language bindings. The returned builder must eventually
 * be freed with [method@Malcontent.SessionLimitsBuilder.free], but can be
 * cleared zero or more times with
 * [method@Malcontent.SessionLimitsBuilder.clear] first.
 *
 * Returns: (transfer full): a new heap-allocated
 *   [struct@Malcontent.SessionLimitsBuilder]
 * Since: 0.5.0
 */
MctSessionLimitsBuilder *
mct_session_limits_builder_new (void)
{
  g_autoptr(MctSessionLimitsBuilder) builder = NULL;

  builder = g_new0 (MctSessionLimitsBuilder, 1);
  mct_session_limits_builder_init (builder);

  return g_steal_pointer (&builder);
}

/**
 * mct_session_limits_builder_copy:
 * @builder: a [struct@Malcontent.SessionLimitsBuilder]
 *
 * Copy the given @builder to a newly-allocated
 * [struct@Malcontent.SessionLimitsBuilder] on the heap.
 *
 * This is safe to use with cleared, stack-allocated
 * [struct@Malcontent.SessionLimitsBuilder]s.
 *
 * Returns: (transfer full): a copy of @builder
 * Since: 0.5.0
 */
MctSessionLimitsBuilder *
mct_session_limits_builder_copy (MctSessionLimitsBuilder *builder)
{
  MctSessionLimitsBuilderReal *_builder = (MctSessionLimitsBuilderReal *) builder;
  g_autoptr(MctSessionLimitsBuilder) copy = NULL;
  MctSessionLimitsBuilderReal *_copy;

  g_return_val_if_fail (builder != NULL, NULL);

  copy = mct_session_limits_builder_new ();
  _copy = (MctSessionLimitsBuilderReal *) copy;

  mct_session_limits_builder_clear (copy);
  _copy->limit_type = _builder->limit_type;
  _copy->set_limit_type = _builder->set_limit_type;

  if (_builder->set_limit_type & MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE)
    {
      _copy->daily_schedule.start_time = _builder->daily_schedule.start_time;
      _copy->daily_schedule.end_time = _builder->daily_schedule.end_time;
    }

  if (_builder->set_limit_type & MCT_SESSION_LIMITS_TYPE_DAILY_LIMIT)
    _copy->daily_limit_secs = _builder->daily_limit_secs;

  _copy->active_extension_start_time_secs = _builder->active_extension_start_time_secs;
  _copy->active_extension_duration_secs = _builder->active_extension_duration_secs;

  return g_steal_pointer (&copy);
}

/**
 * mct_session_limits_builder_free:
 * @builder: a heap-allocated [struct@Malcontent.SessionLimitsBuilder]
 *
 * Free an [struct@Malcontent.SessionLimitsBuilder] originally allocated using
 * [ctor@Malcontent.SessionLimitsBuilder.new].
 *
 * This must not be called on stack-allocated builders initialised using
 * [method@Malcontent.SessionLimitsBuilder.init].
 *
 * Since: 0.5.0
 */
void
mct_session_limits_builder_free (MctSessionLimitsBuilder *builder)
{
  g_return_if_fail (builder != NULL);

  mct_session_limits_builder_clear (builder);
  g_free (builder);
}

/**
 * mct_session_limits_builder_end:
 * @builder: an initialised [struct@Malcontent.SessionLimitsBuilder]
 *
 * Finish constructing an [struct@Malcontent.SessionLimits] with the given
 * @builder, and return it.
 *
 * The [struct@Malcontent.SessionLimitsBuilder] will be cleared as if
 * [method@Malcontent.SessionLimitsBuilder.clear] had been called.
 *
 * Returns: (transfer full): a newly constructed
 *   [struct@Malcontent.SessionLimits]
 * Since: 0.5.0
 */
MctSessionLimits *
mct_session_limits_builder_end (MctSessionLimitsBuilder *builder)
{
  MctSessionLimitsBuilderReal *_builder = (MctSessionLimitsBuilderReal *) builder;
  g_autoptr(MctSessionLimits) session_limits = NULL;

  g_return_val_if_fail (_builder != NULL, NULL);

  /* Build the `MctSessionLimits`. */
  session_limits = g_new0 (MctSessionLimits, 1);
  session_limits->ref_count = 1;
  session_limits->user_id = -1;
  session_limits->limit_type = _builder->limit_type;
  session_limits->set_limit_type = _builder->limit_type;

  if (_builder->set_limit_type & MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE)
    {
      session_limits->daily_start_time = _builder->daily_schedule.start_time;
      session_limits->daily_end_time = _builder->daily_schedule.end_time;
    }
  else
    {
      /* Defaults: */
      session_limits->daily_start_time = 0;
      session_limits->daily_end_time = 24 * 60 * 60;
    }

  if (_builder->set_limit_type & MCT_SESSION_LIMITS_TYPE_DAILY_LIMIT)
    session_limits->daily_limit_secs = _builder->daily_limit_secs;
  else
    session_limits->daily_limit_secs = 24 * 60 * 60;  /* default */

  session_limits->active_extension_start_time_secs = _builder->active_extension_start_time_secs;
  session_limits->active_extension_duration_secs = _builder->active_extension_duration_secs;

  mct_session_limits_builder_clear (builder);

  return g_steal_pointer (&session_limits);
}

/**
 * mct_session_limits_builder_set_from_instance:
 * @builder: an initialised [struct@Malcontent.SessionLimitsBuilder]
 * @limits: a [struct@Malcontent.SessionLimits] to copy into the builder
 *
 * Set all the options in @builder to copies of the options in @limits.
 *
 * Use this to set up a @builder as a clone of @limits, ready to have its
 * options modified further.
 *
 * Since: 0.14.0
 */
void
mct_session_limits_builder_set_from_instance (MctSessionLimitsBuilder *builder,
                                              MctSessionLimits        *limits)
{
  MctSessionLimitsBuilderReal *_builder = (MctSessionLimitsBuilderReal *) builder;

  g_return_if_fail (_builder != NULL);
  g_return_if_fail (limits != NULL);
  g_return_if_fail (limits->ref_count >= 1);

  _builder->limit_type = limits->limit_type;
  _builder->set_limit_type = limits->set_limit_type;

  if (limits->set_limit_type & MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE)
    {
      _builder->daily_schedule.start_time = limits->daily_start_time;
      _builder->daily_schedule.end_time = limits->daily_end_time;
    }

  if (limits->set_limit_type & MCT_SESSION_LIMITS_TYPE_DAILY_LIMIT)
    _builder->daily_limit_secs = limits->daily_limit_secs;

  _builder->active_extension_start_time_secs = limits->active_extension_start_time_secs;
  _builder->active_extension_duration_secs = limits->active_extension_duration_secs;
}

/**
 * mct_session_limits_builder_set_none:
 * @builder: an initialised [struct@Malcontent.SessionLimitsBuilder]
 *
 * Unset any session limits currently set in the @builder.
 *
 * Since: 0.5.0
 */
void
mct_session_limits_builder_set_none (MctSessionLimitsBuilder *builder)
{
  MctSessionLimitsBuilderReal *_builder = (MctSessionLimitsBuilderReal *) builder;

  g_return_if_fail (_builder != NULL);

  /* This will need to free other limit types’ data first in future. */
  _builder->limit_type = MCT_SESSION_LIMITS_TYPE_NONE;
}

/**
 * mct_session_limits_builder_set_daily_schedule:
 * @builder: an initialised [struct@Malcontent.SessionLimitsBuilder]
 * @enforced: true if the limit type is to be enforced, false if the value is
 *   just being stored for future use without being enforced
 * @start_time_secs: number of seconds since midnight when the user’s session
 *   can first start
 * @end_time_secs: number of seconds since midnight when the user’s session can
 *   last end
 *
 * Set the session limits in @builder to be a daily schedule, where sessions are
 * allowed between @start_time_secs and @end_time_secs every day.
 *
 * @start_time_secs and @end_time_secs are given as offsets from the start of
 * the day, in seconds. @end_time_secs must be greater than @start_time_secs.
 * @end_time_secs must be at most `24 * 60 * 60`.
 *
 * This will act in addition to any other session limits.
 *
 * Since: 0.14.0
 */
void
mct_session_limits_builder_set_daily_schedule (MctSessionLimitsBuilder *builder,
                                               gboolean                 enforced,
                                               guint                    start_time_secs,
                                               guint                    end_time_secs)
{
  MctSessionLimitsBuilderReal *_builder = (MctSessionLimitsBuilderReal *) builder;

  g_return_if_fail (_builder != NULL);
  g_return_if_fail (start_time_secs < end_time_secs);
  g_return_if_fail (end_time_secs <= 24 * 60 * 60);

  _builder->set_limit_type |= MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE;
  if (enforced)
    _builder->limit_type |= MCT_SESSION_LIMITS_TYPE_DAILY_SCHEDULE;
  _builder->daily_schedule.start_time = start_time_secs;
  _builder->daily_schedule.end_time = end_time_secs;
}

/**
 * mct_session_limits_builder_set_daily_limit:
 * @builder: an initialised [struct@Malcontent.SessionLimitsBuilder]
 * @enforced: true if the limit type is to be enforced, false if the value is
 *   just being stored for future use without being enforced
 * @daily_limit_secs: maximum length for the user’s active session time each
 *   day, in seconds
 *
 * Set the session limits in @builder to be a daily limit, where the total
 * active session time for the user has a given limit each day.
 *
 * @daily_limit_secs must be at most `24 * 60 * 60`.
 *
 * This will act in addition to any other session limits.
 *
 * Since: 0.14.0
 */
void
mct_session_limits_builder_set_daily_limit (MctSessionLimitsBuilder *builder,
                                            gboolean                 enforced,
                                            unsigned int             daily_limit_secs)
{
  MctSessionLimitsBuilderReal *_builder = (MctSessionLimitsBuilderReal *) builder;

  g_return_if_fail (_builder != NULL);
  g_return_if_fail (daily_limit_secs <= 24 * 60 * 60);

  _builder->set_limit_type |= MCT_SESSION_LIMITS_TYPE_DAILY_LIMIT;
  if (enforced)
    _builder->limit_type |= MCT_SESSION_LIMITS_TYPE_DAILY_LIMIT;
  _builder->daily_limit_secs = daily_limit_secs;
}

/**
 * mct_session_limits_builder_set_active_extension:
 * @builder: an initialised [struct@Malcontent.SessionLimitsBuilder]
 * @start_time_secs: start time of the extension, in seconds since the Unix
 *   epoch
 * @duration_secs: duration of the extension, in seconds; or zero if there is no
 *   active extension
 *
 * Set the screen time extension in @builder to the given period.
 *
 * This is used for recording a screen time extension request which has been
 * granted.
 *
 * If both @start_time_secs and @duration_secs are zero, the screen time
 * extension will be cleared. @duration_secs must otherwise be non-zero.
 *
 * This will act on top of any session limits. If no session limits are set, any
 * extension will be ignored.
 *
 * Since: 0.14.0
 */
void
mct_session_limits_builder_set_active_extension (MctSessionLimitsBuilder *builder,
                                                 uint64_t                 start_time_secs,
                                                 unsigned int             duration_secs)
{
  MctSessionLimitsBuilderReal *_builder = (MctSessionLimitsBuilderReal *) builder;

  g_return_if_fail (_builder != NULL);
  g_return_if_fail (duration_secs <= UINT64_MAX - start_time_secs);
  g_return_if_fail (duration_secs != 0 || start_time_secs == 0);

  _builder->active_extension_start_time_secs = start_time_secs;
  _builder->active_extension_duration_secs = duration_secs;
}
