#include <glib/gi18n.h>
#include <stdlib.h>
#include "settings-dialog.h"
#include "gui-misc.h"
#include "common.h"
#include "gsettings-common.h"
#include "secret-schema.h"
#include "settings-import-export.h"
#include "otp-button-row.h"

struct _SettingsDialog
{
    AdwPreferencesDialog parent;

    OTPClientApplication *app;

    GtkWidget *show_next_otp_switch;
    GtkWidget *show_validity_seconds_switch;
    GtkWidget *validity_color_row;
    GtkWidget *validity_warning_color_row;
    GtkWidget *dark_theme_switch;
    GtkWidget *disable_notifications_switch;
    GtkWidget *auto_lock_switch;
    GtkWidget *inactivity_combo;
    GtkWidget *secret_service_switch;
    GtkWidget *search_provider_switch;
    GtkWidget *search_provider_keyword_entry;
    GtkWidget *clipboard_clear_combo;
    GtkWidget *hide_otps_switch;
#ifdef ENABLE_MINIMIZE_TO_TRAY
    GtkWidget *minimize_to_tray_switch;
#endif
};

G_DEFINE_FINAL_TYPE (SettingsDialog, settings_dialog, ADW_TYPE_PREFERENCES_DIALOG)

/* Forward declarations: the Auto-Lock and Secret Service handlers each need to
 * switch the other off, since the two are mutually exclusive (#279). */
static void on_auto_lock_toggled (GObject *obj, GParamSpec *pspec, SettingsDialog *self);
static void on_secret_service_toggled (GObject *obj, GParamSpec *pspec, SettingsDialog *self);

static void
sync_lock_exclusivity (SettingsDialog *self)
{
    gboolean auto_lock = adw_switch_row_get_active (ADW_SWITCH_ROW (self->auto_lock_switch));
    gboolean secret_service = adw_switch_row_get_active (ADW_SWITCH_ROW (self->secret_service_switch));

    /* A lock is meaningless when the password lives in the keyring (it is
     * bypassed by a restart), so whichever feature is on greys out and disables
     * the other. */
    gtk_widget_set_sensitive (self->auto_lock_switch, !secret_service);
    gtk_widget_set_sensitive (self->secret_service_switch, !auto_lock);
    gtk_widget_set_sensitive (self->inactivity_combo, auto_lock && !secret_service);
}

static void
on_show_next_otp_toggled (GObject        *obj,
                          GParamSpec     *pspec,
                          SettingsDialog *self)
{
    (void) pspec;
    gboolean active = adw_switch_row_get_active (ADW_SWITCH_ROW (obj));
    otpclient_application_set_show_next_otp (self->app, active);
}

static void
on_hide_otps_toggled (GObject        *obj,
                      GParamSpec     *pspec,
                      SettingsDialog *self)
{
    (void) pspec;
    gboolean active = adw_switch_row_get_active (ADW_SWITCH_ROW (obj));
    otpclient_application_set_hide_otps (self->app, active);
}

static void
on_dark_theme_toggled (GObject        *obj,
                       GParamSpec     *pspec,
                       SettingsDialog *self)
{
    (void) pspec;
    gboolean active = adw_switch_row_get_active (ADW_SWITCH_ROW (obj));
    otpclient_application_set_use_dark_theme (self->app, active);

    AdwStyleManager *style_manager = adw_style_manager_get_default ();
    adw_style_manager_set_color_scheme (style_manager,
                                        active ? ADW_COLOR_SCHEME_FORCE_DARK
                                               : ADW_COLOR_SCHEME_DEFAULT);
}

static void
on_disable_notifications_toggled (GObject        *obj,
                                  GParamSpec     *pspec,
                                  SettingsDialog *self)
{
    (void) pspec;
    gboolean active = adw_switch_row_get_active (ADW_SWITCH_ROW (obj));
    otpclient_application_set_disable_notifications (self->app, active);
}

static void
on_auto_lock_toggled (GObject        *obj,
                      GParamSpec     *pspec,
                      SettingsDialog *self)
{
    (void) pspec;
    gboolean active = adw_switch_row_get_active (ADW_SWITCH_ROW (obj));
    otpclient_application_set_auto_lock (self->app, active);

    /* Enabling Auto-Lock turns Secret Service off (#279). */
    if (active &&
        adw_switch_row_get_active (ADW_SWITCH_ROW (self->secret_service_switch)))
    {
        g_signal_handlers_block_by_func (self->secret_service_switch,
                                         on_secret_service_toggled, self);
        adw_switch_row_set_active (ADW_SWITCH_ROW (self->secret_service_switch), FALSE);
        g_signal_handlers_unblock_by_func (self->secret_service_switch,
                                           on_secret_service_toggled, self);
        otpclient_application_set_use_secret_service (self->app, FALSE);
    }

    sync_lock_exclusivity (self);
}

static void
on_inactivity_changed (AdwComboRow    *combo,
                       GParamSpec     *pspec,
                       SettingsDialog *self)
{
    (void) pspec;
    static const gint timeout_values[] = { 60, 120, 300, 600, 900, 1800, 3600 };
    guint selected = adw_combo_row_get_selected (combo);
    if (selected < G_N_ELEMENTS (timeout_values))
        otpclient_application_set_inactivity_timeout (self->app, timeout_values[selected]);
}

static void
present_secret_service_unavailable_dialog (SettingsDialog *self,
                                            const gchar    *err_msg)
{
    g_autofree gchar *body = g_strdup_printf (
        _("OTPClient could not access your system keyring (KWallet, GNOME Keyring, etc.). "
          "The setting has been left disabled. Enable your keyring in the system settings, "
          "then try again.\n\nError: %s"),
        err_msg != NULL ? err_msg : _("unknown error"));

    AdwAlertDialog *alert = ADW_ALERT_DIALOG (
        adw_alert_dialog_new (_("Couldn't enable Secret Service"), body));
    adw_alert_dialog_add_response (alert, "ok", _("OK"));
    adw_alert_dialog_set_default_response (alert, "ok");
    adw_alert_dialog_set_close_response (alert, "ok");
    adw_dialog_present (ADW_DIALOG (alert), GTK_WIDGET (self));
}

static void
on_secret_service_toggled (GObject        *obj,
                           GParamSpec     *pspec,
                           SettingsDialog *self)
{
    (void) pspec;
    gboolean active = adw_switch_row_get_active (ADW_SWITCH_ROW (obj));

    if (!active) {
        otpclient_application_set_use_secret_service (self->app, FALSE);
        sync_lock_exclusivity (self);
        return;
    }

    /* Pre-flight: refuse to enable on a broken keyring rather than letting
     * the user discover later via failed lookups and stored-password loops
     * (issue #446). Sync calls are fine here - the user just clicked the
     * toggle and expects immediate feedback. */
    GError *err = NULL;
    if (otpclient_secret_service_probe (&err)) {
        otpclient_application_set_use_secret_service (self->app, TRUE);

        /* Enabling Secret Service turns Auto-Lock off (#279). */
        if (adw_switch_row_get_active (ADW_SWITCH_ROW (self->auto_lock_switch)))
        {
            g_signal_handlers_block_by_func (self->auto_lock_switch,
                                             on_auto_lock_toggled, self);
            adw_switch_row_set_active (ADW_SWITCH_ROW (self->auto_lock_switch), FALSE);
            g_signal_handlers_unblock_by_func (self->auto_lock_switch,
                                               on_auto_lock_toggled, self);
            otpclient_application_set_auto_lock (self->app, FALSE);
        }

        sync_lock_exclusivity (self);
        return;
    }

    /* Revert the switch without re-entering this handler. */
    g_signal_handlers_block_by_func (obj, on_secret_service_toggled, self);
    adw_switch_row_set_active (ADW_SWITCH_ROW (obj), FALSE);
    g_signal_handlers_unblock_by_func (obj, on_secret_service_toggled, self);

    otpclient_application_set_use_secret_service (self->app, FALSE);
    present_secret_service_unavailable_dialog (self, err != NULL ? err->message : NULL);
    g_clear_error (&err);
}

static void
on_search_provider_toggled (GObject        *obj,
                            GParamSpec     *pspec,
                            SettingsDialog *self)
{
    (void) pspec;
    gboolean active = adw_switch_row_get_active (ADW_SWITCH_ROW (obj));
    otpclient_application_set_search_provider_enabled (self->app, active);
    if (self->search_provider_keyword_entry != NULL)
        gtk_widget_set_sensitive (self->search_provider_keyword_entry, active);
}

static void
on_search_provider_keyword_changed (GObject        *obj,
                                    GParamSpec     *pspec,
                                    SettingsDialog *self)
{
    (void) pspec;
    const gchar *text = gtk_editable_get_text (GTK_EDITABLE (obj));
    g_autofree gchar *trimmed = g_strdup (text ? text : "");
    g_strstrip (trimmed);
    otpclient_application_set_search_provider_keyword (self->app, trimmed);
}

static void
on_clipboard_clear_changed (AdwComboRow    *combo,
                            GParamSpec     *pspec,
                            SettingsDialog *self)
{
    (void) pspec;
    static const guint timeout_values[] = { 0, 10, 15, 30, 60, 120 };
    guint selected = adw_combo_row_get_selected (combo);
    if (selected < G_N_ELEMENTS (timeout_values))
        otpclient_application_set_clipboard_clear_timeout (self->app, timeout_values[selected]);
}

#ifdef ENABLE_MINIMIZE_TO_TRAY
static void
on_minimize_to_tray_toggled (GObject        *obj,
                              GParamSpec     *pspec,
                              SettingsDialog *self)
{
    (void) pspec;
    gboolean active = adw_switch_row_get_active (ADW_SWITCH_ROW (obj));
    otpclient_application_set_minimize_to_tray (self->app, active);
}
#endif

static void
on_show_validity_seconds_toggled (GObject        *obj,
                                   GParamSpec     *pspec,
                                   SettingsDialog *self)
{
    (void) pspec;
    gboolean active = adw_switch_row_get_active (ADW_SWITCH_ROW (obj));
    otpclient_application_set_show_validity_seconds (self->app, active);
    gtk_widget_set_visible (self->validity_color_row, !active);
    gtk_widget_set_visible (self->validity_warning_color_row, !active);
}

static void
on_validity_color_changed (GtkColorDialogButton *button,
                           GParamSpec           *pspec,
                           SettingsDialog       *self)
{
    (void) pspec;
    const GdkRGBA *rgba = gtk_color_dialog_button_get_rgba (button);
    g_autofree gchar *hex = g_strdup_printf ("#%02x%02x%02x",
                                              (guint)(rgba->red * 255),
                                              (guint)(rgba->green * 255),
                                              (guint)(rgba->blue * 255));
    otpclient_application_set_validity_color (self->app, hex);
}

static void
on_validity_warning_color_changed (GtkColorDialogButton *button,
                                   GParamSpec           *pspec,
                                   SettingsDialog       *self)
{
    (void) pspec;
    const GdkRGBA *rgba = gtk_color_dialog_button_get_rgba (button);
    g_autofree gchar *hex = g_strdup_printf ("#%02x%02x%02x",
                                              (guint)(rgba->red * 255),
                                              (guint)(rgba->green * 255),
                                              (guint)(rgba->blue * 255));
    otpclient_application_set_validity_warning_color (self->app, hex);
}

static void
on_export_file_save_complete (GObject      *source,
                              GAsyncResult *result,
                              gpointer      user_data)
{
    GtkFileDialog *dialog = GTK_FILE_DIALOG (source);
    g_autoptr (SettingsDialog) self = SETTINGS_DIALOG (user_data);
    GError *err = NULL;

    GFile *file = gtk_file_dialog_save_finish (dialog, result, &err);
    if (file == NULL) {
        if (!g_error_matches (err, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED))
            g_printerr ("Export settings error: %s\n", err->message);
        g_clear_error (&err);
        return;
    }

    g_autofree gchar *path = g_file_get_path (file);
    g_object_unref (file);

    gchar *json = export_settings_to_json (&err);
    if (json == NULL) {
        AdwAlertDialog *alert = ADW_ALERT_DIALOG (adw_alert_dialog_new (_("Export Failed"), err->message));
        adw_alert_dialog_add_response (alert, "ok", _("OK"));
        adw_dialog_present (ADW_DIALOG (alert), GTK_WIDGET (self));
        g_clear_error (&err);
        return;
    }

    if (!g_file_set_contents (path, json, -1, &err)) {
        AdwAlertDialog *alert = ADW_ALERT_DIALOG (adw_alert_dialog_new (_("Export Failed"), err->message));
        adw_alert_dialog_add_response (alert, "ok", _("OK"));
        adw_dialog_present (ADW_DIALOG (alert), GTK_WIDGET (self));
        g_clear_error (&err);
    }

    gcry_free (json);
}

static void
on_export_settings_clicked (GtkWidget      *button __attribute__((unused)),
                            SettingsDialog *self)
{
    GtkFileDialog *dialog = gtk_file_dialog_new ();
    gtk_file_dialog_set_title (dialog, _("Export Settings"));
    gtk_file_dialog_set_initial_name (dialog, "otpclient-settings.json");
    gtk_file_dialog_save (dialog,
                          GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (self))),
                          NULL,
                          on_export_file_save_complete,
                          g_object_ref (self));
    g_object_unref (dialog);
}

static void
on_import_file_open_complete (GObject      *source,
                              GAsyncResult *result,
                              gpointer      user_data)
{
    GtkFileDialog *dialog = GTK_FILE_DIALOG (source);
    g_autoptr (SettingsDialog) self = SETTINGS_DIALOG (user_data);
    GError *err = NULL;

    GFile *file = gtk_file_dialog_open_finish (dialog, result, &err);
    if (file == NULL) {
        if (!g_error_matches (err, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED))
            g_printerr ("Import settings error: %s\n", err->message);
        g_clear_error (&err);
        return;
    }

    g_autofree gchar *path = g_file_get_path (file);
    g_object_unref (file);

    gchar *contents = NULL;
    if (!g_file_get_contents (path, &contents, NULL, &err)) {
        AdwAlertDialog *alert = ADW_ALERT_DIALOG (adw_alert_dialog_new (_("Import Failed"), err->message));
        adw_alert_dialog_add_response (alert, "ok", _("OK"));
        adw_dialog_present (ADW_DIALOG (alert), GTK_WIDGET (self));
        g_clear_error (&err);
        return;
    }

    if (!import_settings_from_json (contents, &err)) {
        AdwAlertDialog *alert = ADW_ALERT_DIALOG (adw_alert_dialog_new (_("Import Failed"), err->message));
        adw_alert_dialog_add_response (alert, "ok", _("OK"));
        adw_dialog_present (ADW_DIALOG (alert), GTK_WIDGET (self));
        g_clear_error (&err);
        g_free (contents);
        return;
    }
    g_free (contents);

    /* Sync in-memory app state from GSettings and close the dialog so
     * the user sees fresh values when they reopen it. */
    otpclient_application_reload_settings (self->app);
    adw_dialog_close (ADW_DIALOG (self));
}

static void
on_import_settings_clicked (GtkWidget      *button __attribute__((unused)),
                            SettingsDialog *self)
{
    GtkFileDialog *dialog = gtk_file_dialog_new ();
    gtk_file_dialog_set_title (dialog, _("Import Settings"));
    gtk_file_dialog_open (dialog,
                          GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (self))),
                          NULL,
                          on_import_file_open_complete,
                          g_object_ref (self));
    g_object_unref (dialog);
}

/* The token rows just dispatch to window actions so the file-picker, password
 * prompt, db_copy_to call, and last-export-time bump all live in one place
 * (otpclient-window.c) and are shared with the backup-age banner. */
static void
on_backup_tokens_clicked (GtkWidget      *button __attribute__((unused)),
                          SettingsDialog *self)
{
    GtkRoot *root = gtk_widget_get_root (GTK_WIDGET (self));
    if (root != NULL)
        gtk_widget_activate_action (GTK_WIDGET (root), "win.backup-tokens", NULL);
}

static void
on_restore_tokens_clicked (GtkWidget      *button __attribute__((unused)),
                           SettingsDialog *self)
{
    GtkRoot *root = gtk_widget_get_root (GTK_WIDGET (self));
    if (root != NULL)
        gtk_widget_activate_action (GTK_WIDGET (root), "win.restore-tokens", NULL);
}

static void
settings_dialog_init (SettingsDialog *self)
{
    (void) self;
}

static void
settings_dialog_class_init (SettingsDialogClass *klass)
{
    (void) klass;
}

SettingsDialog *
settings_dialog_new (OTPClientApplication *app)
{
    SettingsDialog *self = g_object_new (SETTINGS_TYPE_DIALOG, NULL);
    self->app = app;

    /* Display group */
    AdwPreferencesGroup *display_group = ADW_PREFERENCES_GROUP (adw_preferences_group_new ());
    adw_preferences_group_set_title (display_group, _("Display"));

    self->show_next_otp_switch = adw_switch_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->show_next_otp_switch), _("Show Next OTP"));
    adw_switch_row_set_active (ADW_SWITCH_ROW (self->show_next_otp_switch),
                               otpclient_application_get_show_next_otp (app));
    g_signal_connect (self->show_next_otp_switch, "notify::active",
                      G_CALLBACK (on_show_next_otp_toggled), self);
    adw_preferences_group_add (display_group, self->show_next_otp_switch);

    /* Hide OTPs by default - leaves the cell blank in the list and
     * temporarily reveals the row when clicked (timeout below). */
    gboolean hide_active = otpclient_application_get_hide_otps (app);
    self->hide_otps_switch = adw_switch_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->hide_otps_switch), _("Hide OTPs by Default"));
    adw_action_row_set_subtitle (ADW_ACTION_ROW (self->hide_otps_switch),
                                  _("Hide OTP codes in the list and reveal a row temporarily when clicked"));
    adw_switch_row_set_active (ADW_SWITCH_ROW (self->hide_otps_switch), hide_active);
    g_signal_connect (self->hide_otps_switch, "notify::active",
                      G_CALLBACK (on_hide_otps_toggled), self);
    adw_preferences_group_add (display_group, self->hide_otps_switch);

    self->show_validity_seconds_switch = adw_switch_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->show_validity_seconds_switch),
                                    _("Show Validity in seconds"));
    gboolean validity_seconds_active = otpclient_application_get_show_validity_seconds (app);
    adw_switch_row_set_active (ADW_SWITCH_ROW (self->show_validity_seconds_switch),
                               validity_seconds_active);
    g_signal_connect (self->show_validity_seconds_switch, "notify::active",
                      G_CALLBACK (on_show_validity_seconds_toggled), self);
    adw_preferences_group_add (display_group, self->show_validity_seconds_switch);

    /* Countdown color picker */
    GdkRGBA validity_rgba;
    gdk_rgba_parse (&validity_rgba, otpclient_application_get_validity_color (app));
    GtkColorDialog *color_dialog = gtk_color_dialog_new ();
    GtkWidget *color_button = gtk_color_dialog_button_new (color_dialog);
    gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (color_button), &validity_rgba);
    g_signal_connect (color_button, "notify::rgba",
                      G_CALLBACK (on_validity_color_changed), self);
    self->validity_color_row = adw_action_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->validity_color_row),
                                    _("Countdown Color"));
    adw_action_row_add_suffix (ADW_ACTION_ROW (self->validity_color_row), color_button);
    gtk_widget_set_visible (self->validity_color_row, !validity_seconds_active);
    adw_preferences_group_add (display_group, self->validity_color_row);

    /* Countdown warning color picker */
    GdkRGBA warning_rgba;
    gdk_rgba_parse (&warning_rgba, otpclient_application_get_validity_warning_color (app));
    GtkColorDialog *warning_color_dialog = gtk_color_dialog_new ();
    GtkWidget *warning_color_button = gtk_color_dialog_button_new (warning_color_dialog);
    gtk_color_dialog_button_set_rgba (GTK_COLOR_DIALOG_BUTTON (warning_color_button), &warning_rgba);
    g_signal_connect (warning_color_button, "notify::rgba",
                      G_CALLBACK (on_validity_warning_color_changed), self);
    self->validity_warning_color_row = adw_action_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->validity_warning_color_row),
                                    _("Countdown Warning Color"));
    adw_action_row_add_suffix (ADW_ACTION_ROW (self->validity_warning_color_row), warning_color_button);
    gtk_widget_set_visible (self->validity_warning_color_row, !validity_seconds_active);
    adw_preferences_group_add (display_group, self->validity_warning_color_row);

    self->dark_theme_switch = adw_switch_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->dark_theme_switch), _("Dark Theme"));
    adw_switch_row_set_active (ADW_SWITCH_ROW (self->dark_theme_switch),
                               otpclient_application_get_use_dark_theme (app));
    g_signal_connect (self->dark_theme_switch, "notify::active",
                      G_CALLBACK (on_dark_theme_toggled), self);
    adw_preferences_group_add (display_group, self->dark_theme_switch);

    /* Notifications group */
    AdwPreferencesGroup *notif_group = ADW_PREFERENCES_GROUP (adw_preferences_group_new ());
    adw_preferences_group_set_title (notif_group, _("Notifications"));

    self->disable_notifications_switch = adw_switch_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->disable_notifications_switch),
                                    _("Disable Notifications"));
    adw_switch_row_set_active (ADW_SWITCH_ROW (self->disable_notifications_switch),
                               otpclient_application_get_disable_notifications (app));
    g_signal_connect (self->disable_notifications_switch, "notify::active",
                      G_CALLBACK (on_disable_notifications_toggled), self);
    adw_preferences_group_add (notif_group, self->disable_notifications_switch);

    /* Security group */
    AdwPreferencesGroup *security_group = ADW_PREFERENCES_GROUP (adw_preferences_group_new ());
    adw_preferences_group_set_title (security_group, _("Security"));

    self->auto_lock_switch = adw_switch_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->auto_lock_switch), _("Auto-Lock"));
    adw_switch_row_set_active (ADW_SWITCH_ROW (self->auto_lock_switch),
                               otpclient_application_get_auto_lock (app));
    g_signal_connect (self->auto_lock_switch, "notify::active",
                      G_CALLBACK (on_auto_lock_toggled), self);
    adw_preferences_group_add (security_group, self->auto_lock_switch);

    const char * const timeout_items[] = {
        N_("1 minute"), N_("2 minutes"), N_("5 minutes"), N_("10 minutes"),
        N_("15 minutes"), N_("30 minutes"), N_("1 hour"), NULL
    };
    static const gint inactivity_values[] = { 60, 120, 300, 600, 900, 1800, 3600 };
    const char *timeout_translated[G_N_ELEMENTS (inactivity_values) + 1];
    for (guint i = 0; i < G_N_ELEMENTS (inactivity_values); i++)
        timeout_translated[i] = _(timeout_items[i]);
    timeout_translated[G_N_ELEMENTS (inactivity_values)] = NULL;
    GtkStringList *timeout_model = gtk_string_list_new (timeout_translated);
    self->inactivity_combo = adw_combo_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->inactivity_combo),
                                    _("Inactivity Timeout"));
    adw_combo_row_set_model (ADW_COMBO_ROW (self->inactivity_combo), G_LIST_MODEL (timeout_model));
    /* Sync the selection to the current stored value so reopening the dialog
     * shows what's actually configured rather than always defaulting to row 0. */
    guint current_inactivity = (guint) otpclient_application_get_inactivity_timeout (app);
    for (guint i = 0; i < G_N_ELEMENTS (inactivity_values); i++) {
        if ((guint) inactivity_values[i] == current_inactivity) {
            adw_combo_row_set_selected (ADW_COMBO_ROW (self->inactivity_combo), i);
            break;
        }
    }
    g_signal_connect (self->inactivity_combo, "notify::selected",
                      G_CALLBACK (on_inactivity_changed), self);
    adw_preferences_group_add (security_group, self->inactivity_combo);

    const char * const clip_timeout_items[] = {
        N_("Never"), N_("10 seconds"), N_("15 seconds"), N_("30 seconds"),
        N_("1 minute"), N_("2 minutes"), NULL
    };
    static const guint clip_timeout_values[] = { 0, 10, 15, 30, 60, 120 };
    const char *clip_translated[G_N_ELEMENTS (clip_timeout_values) + 1];
    for (guint i = 0; i < G_N_ELEMENTS (clip_timeout_values); i++)
        clip_translated[i] = _(clip_timeout_items[i]);
    clip_translated[G_N_ELEMENTS (clip_timeout_values)] = NULL;
    GtkStringList *clip_timeout_model = gtk_string_list_new (clip_translated);
    self->clipboard_clear_combo = adw_combo_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->clipboard_clear_combo),
                                    _("Clear Clipboard After"));
    adw_action_row_set_subtitle (ADW_ACTION_ROW (self->clipboard_clear_combo),
                                  _("Automatically clear the clipboard after copying an OTP code from this window"));
    adw_combo_row_set_model (ADW_COMBO_ROW (self->clipboard_clear_combo), G_LIST_MODEL (clip_timeout_model));
    /* Select current value */
    guint current_clip_timeout = otpclient_application_get_clipboard_clear_timeout (app);
    for (guint i = 0; i < G_N_ELEMENTS (clip_timeout_values); i++) {
        if (clip_timeout_values[i] == current_clip_timeout) {
            adw_combo_row_set_selected (ADW_COMBO_ROW (self->clipboard_clear_combo), i);
            break;
        }
    }
    g_signal_connect (self->clipboard_clear_combo, "notify::selected",
                      G_CALLBACK (on_clipboard_clear_changed), self);
    adw_preferences_group_add (security_group, self->clipboard_clear_combo);

    self->secret_service_switch = adw_switch_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->secret_service_switch),
                                    _("Use Secret Service"));
    adw_action_row_set_subtitle (ADW_ACTION_ROW (self->secret_service_switch),
                                  _("Store the database password in the system keyring so the app unlocks automatically after login. Also enables searching across all databases at once"));
    adw_switch_row_set_active (ADW_SWITCH_ROW (self->secret_service_switch),
                               otpclient_application_get_use_secret_service (app));
    g_signal_connect (self->secret_service_switch, "notify::active",
                      G_CALLBACK (on_secret_service_toggled), self);
    adw_preferences_group_add (security_group, self->secret_service_switch);

    /* #279: Auto-Lock and Secret Service are mutually exclusive. Reconcile a
     * legacy profile that has both enabled (Secret Service wins, since the lock
     * is bypassable via restart when the password is in the keyring), then set
     * the initial greyed-out state. */
    if (otpclient_application_get_auto_lock (app) &&
        otpclient_application_get_use_secret_service (app))
    {
        g_signal_handlers_block_by_func (self->auto_lock_switch,
                                         on_auto_lock_toggled, self);
        adw_switch_row_set_active (ADW_SWITCH_ROW (self->auto_lock_switch), FALSE);
        g_signal_handlers_unblock_by_func (self->auto_lock_switch,
                                           on_auto_lock_toggled, self);
        otpclient_application_set_auto_lock (self->app, FALSE);
    }
    sync_lock_exclusivity (self);

    /* Integration group */
    AdwPreferencesGroup *integration_group = ADW_PREFERENCES_GROUP (adw_preferences_group_new ());
    adw_preferences_group_set_title (integration_group, _("Integration"));
    adw_preferences_group_set_description (integration_group,
                                           _("Type the keyword followed by a space and your search to surface OTP results in GNOME Shell or KDE KRunner (e.g. \"otp github\"). Changes to the keyword take effect after logging out and back in."));

    gboolean sp_enabled = otpclient_application_get_search_provider_enabled (app);

    self->search_provider_switch = adw_switch_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->search_provider_switch),
                                    _("Search Provider"));
    adw_switch_row_set_active (ADW_SWITCH_ROW (self->search_provider_switch), sp_enabled);
    g_signal_connect (self->search_provider_switch, "notify::active",
                      G_CALLBACK (on_search_provider_toggled), self);
    adw_preferences_group_add (integration_group, self->search_provider_switch);

    self->search_provider_keyword_entry = adw_entry_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->search_provider_keyword_entry),
                                    _("Search Provider Keyword"));
    {
        const gchar *kw = otpclient_application_get_search_provider_keyword (app);
        gtk_editable_set_text (GTK_EDITABLE (self->search_provider_keyword_entry), kw ? kw : "");
    }
    /* AdwEntryRow proxies to an internal GtkText; setting max_length there gives
     * us a hard typing cap that matches the daemon's defensive truncation. */
    {
        GtkEditable *delegate = gtk_editable_get_delegate (GTK_EDITABLE (self->search_provider_keyword_entry));
        if (GTK_IS_TEXT (delegate))
            gtk_text_set_max_length (GTK_TEXT (delegate), OTPCLIENT_SEARCH_KEYWORD_MAX_LEN);
    }
    gtk_widget_set_sensitive (self->search_provider_keyword_entry, sp_enabled);
    g_signal_connect (self->search_provider_keyword_entry, "notify::text",
                      G_CALLBACK (on_search_provider_keyword_changed), self);
    adw_preferences_group_add (integration_group, self->search_provider_keyword_entry);

#ifdef ENABLE_MINIMIZE_TO_TRAY
    self->minimize_to_tray_switch = adw_switch_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->minimize_to_tray_switch),
                                    _("Minimize to Tray"));
    adw_switch_row_set_active (ADW_SWITCH_ROW (self->minimize_to_tray_switch),
                               otpclient_application_get_minimize_to_tray (app));
    g_signal_connect (self->minimize_to_tray_switch, "notify::active",
                      G_CALLBACK (on_minimize_to_tray_toggled), self);
    adw_preferences_group_add (integration_group, self->minimize_to_tray_switch);
#endif

    /* Backup group - covers both app preferences (GSettings JSON) and the
     * encrypted token database. The token rows dispatch to window actions so
     * the same code path also serves the backup-age banner button. */
    AdwPreferencesGroup *backup_group = ADW_PREFERENCES_GROUP (adw_preferences_group_new ());
    adw_preferences_group_set_title (backup_group, _("Backup"));
    adw_preferences_group_set_description (backup_group,
        _("Save and restore your application preferences and your token database."));

    GtkWidget *backup_settings_row = otp_button_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (backup_settings_row), _("Back up app settings"));
    g_signal_connect (backup_settings_row, "activated",
                      G_CALLBACK (on_export_settings_clicked), self);
    adw_preferences_group_add (backup_group, backup_settings_row);

    GtkWidget *restore_settings_row = otp_button_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (restore_settings_row), _("Restore app settings"));
    g_signal_connect (restore_settings_row, "activated",
                      G_CALLBACK (on_import_settings_clicked), self);
    adw_preferences_group_add (backup_group, restore_settings_row);

    GtkWidget *backup_tokens_row = otp_button_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (backup_tokens_row), _("Back up tokens"));
    g_signal_connect (backup_tokens_row, "activated",
                      G_CALLBACK (on_backup_tokens_clicked), self);
    adw_preferences_group_add (backup_group, backup_tokens_row);

    GtkWidget *restore_tokens_row = otp_button_row_new ();
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (restore_tokens_row), _("Restore tokens"));
    g_signal_connect (restore_tokens_row, "activated",
                      G_CALLBACK (on_restore_tokens_clicked), self);
    adw_preferences_group_add (backup_group, restore_tokens_row);

    /* Add page */
    AdwPreferencesPage *page = ADW_PREFERENCES_PAGE (adw_preferences_page_new ());
    adw_preferences_page_set_title (page, _("Settings"));
    adw_preferences_page_set_icon_name (page, "preferences-system-symbolic");
    adw_preferences_page_add (page, display_group);
    adw_preferences_page_add (page, notif_group);
    adw_preferences_page_add (page, security_group);
    adw_preferences_page_add (page, integration_group);
    adw_preferences_page_add (page, backup_group);

    adw_preferences_dialog_add (ADW_PREFERENCES_DIALOG (self), page);

    return self;
}
