// SPDX-FileCopyrightText: 2010-2026 Marc-Antoine Perennou <Marc-Antoine@Perennou.com>
// SPDX-License-Identifier: BSD-2-Clause

#include "gpaste-prompt-adw.h"

#include <gpaste-3/gpaste-util.h>

/* The libadwaita half of the prompt contract: everything here is widgets and
 * the answers they produce. What to do with those answers — which passphrase
 * unlocks what, what gets imported, when the keyring is written — stays in
 * gpaste-storage-migration.c, which never learns that any of this exists. */

struct _GPastePromptAdw
{
    GObject parent_instance;

    /* Anchors the dialogs. Owned by main(), which outlives us. */
    GtkApplication *application;
};

static void g_paste_prompt_adw_prompt_iface_init (GPastePromptInterface *iface);

G_PASTE_DEFINE_TYPE_WITH_INTERFACE (PromptAdw, prompt_adw, G_TYPE_OBJECT,
                                    G_PASTE_TYPE_PROMPT, g_paste_prompt_adw_prompt_iface_init)

/*
 * The migration dialog
 */

typedef struct
{
    GPastePromptRequest *request;

    GtkWindow           *window;
    GtkWidget           *apply;
    AdwComboRow         *backend_row;
    AdwSwitchRow        *import_row;
    AdwSwitchRow        *cleanup_row;
    AdwBanner           *warning;

    GPasteStorage        current;

    /* The backends offered by the combo, in display order. */
    const GPasteStorage *backends;
    guint                n_backends;
} MigrationDialog;

/* Answering from "destroy" is what makes every way of losing the window an
 * answer: gtk_window_destroy() does not emit close-request, and neither does the
 * GtkApplication shutdown that tears the last window down when the daemon loses
 * its D-Bus name — so a dialog up at that moment would never answer, and the
 * caller's `done` would never run. Replying already claimed the request, so this
 * is a no-op on that path.
 *
 * "destroy" and not the teardown below: a destroy notify runs from finalize, so
 * anything still holding a reference on the window at that point (an animation,
 * an a11y peer, a queued frame callback) would defer the answer for as long as
 * it holds it — with the whole daemon startup waiting on it. */
static void
migration_dialog_dismiss (GtkWindow *window G_GNUC_UNUSED,
                          gpointer   user_data)
{
    MigrationDialog *self = user_data;

    g_paste_prompt_request_dismiss (self->request);
}

/* The widgets that carry @self are gone by now, so nothing can reach it again. */
static void
migration_dialog_free (gpointer data)
{
    MigrationDialog *self = data;

    g_clear_object (&self->request);
    g_free (self);
}

/* The combo lists the backends in a built-at-runtime order; map both ways. */
static GPasteStorage
backend_for_index (MigrationDialog *self,
                   guint            index)
{
    return (index < self->n_backends) ? self->backends[index] : G_PASTE_STORAGE_NOOP;
}

static guint
index_for_backend (MigrationDialog *self,
                   GPasteStorage    storage_kind)
{
    for (guint i = 0; i < self->n_backends; ++i)
    {
        if (self->backends[i] == storage_kind)
            return i;
    }

    return 0;
}

/* Whether the backend in use is one this build can construct, hence one the
 * combo lists. It is not, for a history stored by a flavour built out of this
 * copy of GPaste (an .xmls or a .db opened by a build without encryption or
 * sqlite): the dialog then has nothing to preselect and nothing to keep, and
 * says so by offering neither. */
static gboolean
current_is_offered (MigrationDialog *self)
{
    for (guint i = 0; i < self->n_backends; ++i)
    {
        if (self->backends[i] == self->current)
            return TRUE;
    }

    return FALSE;
}

static void
update_state (MigrationDialog *self)
{
    guint selected = adw_combo_row_get_selected (self->backend_row);
    gboolean chosen_anything = (selected < self->n_backends);
    GPasteStorage chosen = backend_for_index (self, selected);
    gboolean backend_changes = chosen_anything && g_paste_prompt_backend_changes (self->current, chosen);
    gboolean import_possible = chosen_anything && g_paste_prompt_can_import (self->current, chosen);

    /* Nothing picked yet, which only the no-preselection case above can mean:
     * there is no answer to apply until the user names a backend. */
    gtk_widget_set_sensitive (self->apply, chosen_anything);

    gtk_widget_set_sensitive (GTK_WIDGET (self->import_row), import_possible);
    if (!import_possible)
        adw_switch_row_set_active (self->import_row, FALSE);

    gtk_widget_set_sensitive (GTK_WIDGET (self->cleanup_row), backend_changes);
    if (!backend_changes)
        adw_switch_row_set_active (self->cleanup_row, FALSE);

    /* Deleting the old data without importing it first throws it away. */
    adw_banner_set_revealed (self->warning,
                             adw_switch_row_get_active (self->cleanup_row) &&
                             !adw_switch_row_get_active (self->import_row));
}

static void
on_state_changed (GObject    *object G_GNUC_UNUSED,
                  GParamSpec *pspec  G_GNUC_UNUSED,
                  gpointer    user_data)
{
    update_state (user_data);
}

/* AdwComboRow ellipsizes the backend labels — both the dropdown rows and the
 * GtkInscription previewing the current selection — and offers no property to
 * stop it. Give it our own factory of plain GtkLabels instead, which it uses for
 * the dropdown rows and the selected-value preview alike, so the longer backend
 * names stay readable. */
static void
backend_label_setup (GtkSignalListItemFactory *factory G_GNUC_UNUSED,
                     GtkListItem              *item,
                     gpointer                  user_data G_GNUC_UNUSED)
{
    GtkWidget *label = gtk_label_new (NULL);

    gtk_label_set_xalign (GTK_LABEL (label), 0.0);
    gtk_list_item_set_child (item, label);
}

static void
backend_label_bind (GtkSignalListItemFactory *factory G_GNUC_UNUSED,
                    GtkListItem              *item,
                    gpointer                  user_data G_GNUC_UNUSED)
{
    GtkStringObject *string = gtk_list_item_get_item (item);

    gtk_label_set_label (GTK_LABEL (gtk_list_item_get_child (item)), gtk_string_object_get_string (string));
}

static void
on_apply (GtkButton *button G_GNUC_UNUSED,
          gpointer   user_data)
{
    MigrationDialog *self = user_data;

    g_paste_prompt_request_reply_migration (self->request,
                                            backend_for_index (self, adw_combo_row_get_selected (self->backend_row)),
                                            adw_switch_row_get_active (self->import_row),
                                            adw_switch_row_get_active (self->cleanup_row));

    gtk_window_destroy (self->window);
}

/* "Nothing changes, and stop asking": the backend in use, no import, no
 * cleanup. A reply rather than the dismissal it sits next to, which is the whole
 * point -- a dismissal leaves the revision alone and the dialog comes back on
 * the next start. Deliberately answers with the *current* backend rather than
 * whatever the combo is showing: this button undoes the fiddling too. */
static void
on_keep (GtkButton *button G_GNUC_UNUSED,
         gpointer   user_data)
{
    MigrationDialog *self = user_data;

    g_paste_prompt_request_reply_migration (self->request, self->current, FALSE, FALSE);

    gtk_window_destroy (self->window);
}

/* Escape closes the dialog like its close button; the teardown above is what
 * turns that into a dismissal. */
static gboolean
on_key_pressed (GtkEventControllerKey *controller G_GNUC_UNUSED,
                guint                  keyval,
                guint                  keycode    G_GNUC_UNUSED,
                GdkModifierType        state      G_GNUC_UNUSED,
                gpointer               user_data)
{
    MigrationDialog *self = user_data;

    if (keyval != GDK_KEY_Escape)
        return GDK_EVENT_PROPAGATE;

    gtk_window_close (self->window);
    return GDK_EVENT_STOP;
}

static void
g_paste_prompt_adw_migration (GPastePrompt        *prompt,
                              GPastePromptRequest *request)
{
    MigrationDialog *self = g_new0 (MigrationDialog, 1);

    self->request = g_object_ref (request);
    self->current = g_paste_prompt_request_get_current (request);
    self->backends = g_paste_prompt_request_get_offered (request, &self->n_backends);

    GtkWidget *window = adw_application_window_new (G_PASTE_PROMPT_ADW (prompt)->application);

    self->window = GTK_WINDOW (window);
    gtk_window_set_title (self->window, g_paste_prompt_text (G_PASTE_PROMPT_TEXT_MIGRATION_TITLE));
    gtk_window_set_icon_name (self->window, G_PASTE_ICON_NAME);
    gtk_window_set_default_size (self->window, 480, -1);
    gtk_window_set_modal (self->window, TRUE);

    GtkWidget *apply = self->apply = gtk_button_new_with_label (g_paste_prompt_text (G_PASTE_PROMPT_TEXT_MIGRATION_ACCEPT));

    gtk_widget_add_css_class (apply, "suggested-action");
    g_signal_connect (apply, "clicked", G_CALLBACK (on_apply), self);

    GtkWidget *header = adw_header_bar_new ();

    /* Only when there is something to keep: the button answers with the backend
     * in use, and one this build cannot construct is not an answer the reply
     * would accept -- it would be turned into the very dismissal the button
     * exists to avoid. Offering nothing there is the honest version of that. */
    if (current_is_offered (self))
    {
        GtkWidget *keep = gtk_button_new_with_label (g_paste_prompt_text (G_PASTE_PROMPT_TEXT_MIGRATION_KEEP));

        g_signal_connect (keep, "clicked", G_CALLBACK (on_keep), self);

        /* Where a cancel button would sit, because that is where someone who
         * wants out of this dialog looks -- and getting out without losing the
         * history to a backend they never chose is what they want. */
        adw_header_bar_pack_start (ADW_HEADER_BAR (header), keep);
    }

    adw_header_bar_pack_end (ADW_HEADER_BAR (header), apply);

    GtkWidget *warning = adw_banner_new (g_paste_prompt_text (G_PASTE_PROMPT_TEXT_CLEANUP_WARNING));

    self->warning = ADW_BANNER (warning);

    g_autoptr (GtkStringList) backends = gtk_string_list_new (NULL);

    for (guint i = 0; i < self->n_backends; ++i)
        gtk_string_list_append (backends, g_paste_prompt_storage_label (self->backends[i]));

    GtkWidget *backend_row = adw_combo_row_new ();

    self->backend_row = ADW_COMBO_ROW (backend_row);
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (backend_row), g_paste_prompt_text (G_PASTE_PROMPT_TEXT_STORAGE_BACKEND));
    adw_combo_row_set_model (self->backend_row, G_LIST_MODEL (backends));

    /* No preselection when the backend in use is not on the list: picking one
     * for the user would have the dialog claim a backend they never chose, and
     * the first row is as wrong a claim as any. Nothing is selected until they
     * say so, and Apply stays insensitive until they do. */
    adw_combo_row_set_selected (self->backend_row,
                                (current_is_offered (self)) ? index_for_backend (self, self->current) : GTK_INVALID_LIST_POSITION);

    g_autoptr (GtkListItemFactory) factory = gtk_signal_list_item_factory_new ();

    g_signal_connect (factory, "setup", G_CALLBACK (backend_label_setup), NULL);
    g_signal_connect (factory, "bind", G_CALLBACK (backend_label_bind), NULL);
    adw_combo_row_set_factory (self->backend_row, factory);

    GtkWidget *import_row = adw_switch_row_new ();

    self->import_row = ADW_SWITCH_ROW (import_row);
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (import_row), g_paste_prompt_text (G_PASTE_PROMPT_TEXT_IMPORT));
    adw_action_row_set_subtitle (ADW_ACTION_ROW (import_row), g_paste_prompt_text (G_PASTE_PROMPT_TEXT_IMPORT_SUBTITLE));

    GtkWidget *cleanup_row = adw_switch_row_new ();

    self->cleanup_row = ADW_SWITCH_ROW (cleanup_row);
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (cleanup_row), g_paste_prompt_text (G_PASTE_PROMPT_TEXT_CLEANUP));
    adw_action_row_set_subtitle (ADW_ACTION_ROW (cleanup_row), g_paste_prompt_text (G_PASTE_PROMPT_TEXT_CLEANUP_SUBTITLE));

    GtkWidget *group = adw_preferences_group_new ();

    adw_preferences_group_set_description (ADW_PREFERENCES_GROUP (group),
                                           g_paste_prompt_text (G_PASTE_PROMPT_TEXT_MIGRATION_DESCRIPTION));
    adw_preferences_group_add (ADW_PREFERENCES_GROUP (group), backend_row);
    adw_preferences_group_add (ADW_PREFERENCES_GROUP (group), import_row);
    adw_preferences_group_add (ADW_PREFERENCES_GROUP (group), cleanup_row);

    GtkWidget *page = adw_preferences_page_new ();

    adw_preferences_page_add (ADW_PREFERENCES_PAGE (page), ADW_PREFERENCES_GROUP (group));

    GtkWidget *content = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);

    gtk_box_append (GTK_BOX (content), warning);
    gtk_box_append (GTK_BOX (content), page);

    GtkWidget *toolbar = adw_toolbar_view_new ();

    adw_toolbar_view_add_top_bar (ADW_TOOLBAR_VIEW (toolbar), header);
    adw_toolbar_view_set_content (ADW_TOOLBAR_VIEW (toolbar), content);

    adw_application_window_set_content (ADW_APPLICATION_WINDOW (window), toolbar);

    g_signal_connect (backend_row, "notify::selected", G_CALLBACK (on_state_changed), self);
    g_signal_connect (import_row, "notify::active", G_CALLBACK (on_state_changed), self);
    g_signal_connect (cleanup_row, "notify::active", G_CALLBACK (on_state_changed), self);
    g_signal_connect (window, "destroy", G_CALLBACK (migration_dialog_dismiss), self);
    g_object_set_data_full (G_OBJECT (window), "gpaste-migration-dialog", self, migration_dialog_free);

    GtkEventController *key_controller = gtk_event_controller_key_new ();

    g_signal_connect (key_controller, "key-pressed", G_CALLBACK (on_key_pressed), self);
    gtk_widget_add_controller (window, key_controller);

    update_state (self);

    gtk_window_present (self->window);
}

/*
 * The passphrase prompt
 */

typedef struct
{
    GPastePromptRequest *request;

    GtkWindow           *window;
    GtkEditable         *entry;
    GtkEditable         *confirm_entry;
    GtkWidget           *remember;
    /* Whether the keyring holds anything, i.e. whether an off switch is a
     * request to forget rather than just an absence. Not where the switch
     * started: a retry after a mistyped passphrase starts it off having
     * carried an explicit "forget" forward, and the two must not be confused. */
    gboolean             remembered;
    GtkWidget           *ok;

    /* Only built when setting a new passphrase (confirm): the strength meter and
     * the row carrying its rating/hint. */
    GtkLevelBar         *strength;
    GtkWidget           *strength_row;
} PassphraseDialog;

/* See migration_dialog_dismiss(): answering from "destroy" is what makes every
 * way of losing the window an answer, without waiting on finalize. */
static void
passphrase_dialog_dismiss (GtkWindow *window G_GNUC_UNUSED,
                           gpointer   user_data)
{
    PassphraseDialog *self = user_data;

    g_paste_prompt_request_dismiss (self->request);
}

static void
passphrase_dialog_free (gpointer data)
{
    PassphraseDialog *self = data;

    g_clear_object (&self->request);
    g_free (self);
}

static void
passphrase_update_ok (PassphraseDialog *self)
{
    /* The confirmation entry only exists when one is being asked for, which is
     * exactly what the shared rule wants told apart. */
    gtk_widget_set_sensitive (self->ok,
                              g_paste_prompt_passphrase_is_complete (gtk_editable_get_text (self->entry),
                                                                     self->confirm_entry
                                                                     ? gtk_editable_get_text (self->confirm_entry)
                                                                     : NULL));
}

static void
on_passphrase_changed (GtkEditable *editable,
                       gpointer     user_data)
{
    PassphraseDialog *self = user_data;

    /* The red hint flags the previous wrong attempt; clear it as soon as the
     * user amends the passphrase so it does not bleed into the next try. */
    gtk_widget_remove_css_class (GTK_WIDGET (self->entry), "error");

    /* Reflect the strength of the new passphrase as it is typed — but only when
     * it is the passphrase that changed. Typing the confirmation re-rates a
     * string that did not move, and rating means a cracklib dictionary pass. */
    if (self->strength && editable == self->entry)
    {
        g_autofree gchar *hint = NULL;
        guint strength = g_paste_prompt_passphrase_strength (gtk_editable_get_text (self->entry), &hint);

        gtk_level_bar_set_value (self->strength, strength);
        adw_action_row_set_subtitle (ADW_ACTION_ROW (self->strength_row), hint ? hint : "");
    }

    passphrase_update_ok (self);
}

static void on_passphrase_ok (GtkButton *button,
                              gpointer   user_data);

/* Enter is only an answer once the prompt would accept one, which is exactly
 * what the OK button's sensitivity tracks. */
static void
on_passphrase_activated (AdwEntryRow *row G_GNUC_UNUSED,
                         gpointer     user_data)
{
    PassphraseDialog *self = user_data;

    if (gtk_widget_get_sensitive (self->ok))
        on_passphrase_ok (NULL, self);
}

static void
on_passphrase_ok (GtkButton *button G_GNUC_UNUSED,
                  gpointer   user_data)
{
    PassphraseDialog *self = user_data;
    GPasteStorageRemember remember = G_PASTE_STORAGE_REMEMBER_UNCHANGED;

    /* Only report the choice: the keyring is written by whoever established that
     * this passphrase is the right one, which cannot be known from here. */
    if (self->remember && adw_switch_row_get_active (ADW_SWITCH_ROW (self->remember)))
        remember = G_PASTE_STORAGE_REMEMBER_YES;
    else if (self->remembered)
        remember = G_PASTE_STORAGE_REMEMBER_NO;

    /* Reply while the entry text is still alive; the reply copies it. */
    g_paste_prompt_request_reply_passphrase (self->request, gtk_editable_get_text (self->entry), remember);
    gtk_window_destroy (self->window);
}

/* Escape closes the prompt like its close button; the teardown is what turns
 * that into a dismissal. */
static gboolean
on_passphrase_key_pressed (GtkEventControllerKey *controller G_GNUC_UNUSED,
                           guint                  keyval,
                           guint                  keycode    G_GNUC_UNUSED,
                           GdkModifierType        state      G_GNUC_UNUSED,
                           gpointer               user_data)
{
    PassphraseDialog *self = user_data;

    if (keyval != GDK_KEY_Escape)
        return GDK_EVENT_PROPAGATE;

    gtk_window_close (self->window);
    return GDK_EVENT_STOP;
}

static void
g_paste_prompt_adw_passphrase (GPastePrompt        *prompt,
                               GPastePromptRequest *request)
{
    PassphraseDialog *self = g_new0 (PassphraseDialog, 1);
    gboolean confirm = g_paste_prompt_request_get_confirm (request);
    const gchar *error_message = g_paste_prompt_request_get_error_message (request);

    self->request = g_object_ref (request);

    GtkWidget *window = adw_application_window_new (G_PASTE_PROMPT_ADW (prompt)->application);

    self->window = GTK_WINDOW (window);
    gtk_window_set_title (self->window, g_paste_prompt_text (G_PASTE_PROMPT_TEXT_PASSPHRASE_TITLE));
    gtk_window_set_icon_name (self->window, G_PASTE_ICON_NAME);
    gtk_window_set_default_size (self->window, 420, -1);
    gtk_window_set_modal (self->window, TRUE);

    GtkWidget *ok = gtk_button_new_with_label (g_paste_prompt_text (confirm ? G_PASTE_PROMPT_TEXT_PASSPHRASE_CONFIRM_ACCEPT : G_PASTE_PROMPT_TEXT_PASSPHRASE_ACCEPT));

    self->ok = ok;
    gtk_widget_add_css_class (ok, "suggested-action");
    gtk_widget_set_sensitive (ok, FALSE);
    g_signal_connect (ok, "clicked", G_CALLBACK (on_passphrase_ok), self);

    GtkWidget *header = adw_header_bar_new ();

    adw_header_bar_pack_end (ADW_HEADER_BAR (header), ok);

    GtkWidget *entry = adw_password_entry_row_new ();

    self->entry = GTK_EDITABLE (entry);
    adw_preferences_row_set_title (ADW_PREFERENCES_ROW (entry), g_paste_prompt_text (G_PASTE_PROMPT_TEXT_PASSPHRASE));
    g_signal_connect (entry, "changed", G_CALLBACK (on_passphrase_changed), self);
    /* Enter submits, as it does in the shell prompt: an unlock that only takes
     * the mouse is an unlock the daemon waits on for no reason. */
    g_signal_connect (entry, "entry-activated", G_CALLBACK (on_passphrase_activated), self);

    GtkWidget *group = adw_preferences_group_new ();

    adw_preferences_group_add (ADW_PREFERENCES_GROUP (group), entry);

    /* Re-prompt after a wrong passphrase: flag the entry. What went wrong is
     * said in the description below, alongside the standing explanation rather
     * than in place of it. */
    if (error_message)
        gtk_widget_add_css_class (entry, "error");

    if (confirm)
    {
        GtkWidget *confirm_entry = adw_password_entry_row_new ();

        self->confirm_entry = GTK_EDITABLE (confirm_entry);
        adw_preferences_row_set_title (ADW_PREFERENCES_ROW (confirm_entry), g_paste_prompt_text (G_PASTE_PROMPT_TEXT_PASSPHRASE_CONFIRM));
        g_signal_connect (confirm_entry, "changed", G_CALLBACK (on_passphrase_changed), self);
        /* Enter submits from here too: this is the field the user finishes
         * typing in, and the shell prompt takes it in both of its entries. */
        g_signal_connect (confirm_entry, "entry-activated", G_CALLBACK (on_passphrase_activated), self);
        adw_preferences_group_add (ADW_PREFERENCES_GROUP (group), confirm_entry);

        GtkWidget *strength_row = adw_action_row_new ();

        adw_preferences_row_set_title (ADW_PREFERENCES_ROW (strength_row), g_paste_prompt_text (G_PASTE_PROMPT_TEXT_PASSPHRASE_STRENGTH));

#ifdef G_PASTE_ENABLE_PWQUALITY
        /* Rate the new passphrase as it is typed, with the rating or
         * libpwquality's advice as the subtitle and a colour-graded meter. */
        self->strength_row = strength_row;

        GtkWidget *strength = gtk_level_bar_new ();

        self->strength = GTK_LEVEL_BAR (strength);
        gtk_level_bar_set_min_value (self->strength, 0);
        gtk_level_bar_set_max_value (self->strength, G_PASTE_PROMPT_STRENGTH_MAX);
        /* Colour the meter red → orange → green as the rating climbs. */
        gtk_level_bar_add_offset_value (self->strength, GTK_LEVEL_BAR_OFFSET_LOW, 1);
        gtk_level_bar_add_offset_value (self->strength, GTK_LEVEL_BAR_OFFSET_HIGH, 3);
        gtk_level_bar_add_offset_value (self->strength, GTK_LEVEL_BAR_OFFSET_FULL, G_PASTE_PROMPT_STRENGTH_MAX);
        gtk_widget_set_valign (strength, GTK_ALIGN_CENTER);
        gtk_widget_set_size_request (strength, 120, -1);
        adw_action_row_add_suffix (ADW_ACTION_ROW (strength_row), strength);
#else
        /* Built without libpwquality, so there is no rating to give. Say so
         * rather than leave the row out: someone choosing a passphrase should
         * know it is not being judged, instead of reading a silent absence as
         * approval. */
        adw_action_row_set_subtitle (ADW_ACTION_ROW (strength_row), g_paste_prompt_text (G_PASTE_PROMPT_TEXT_PASSPHRASE_STRENGTH_UNAVAILABLE));
        gtk_widget_set_sensitive (strength_row, FALSE);
#endif

        adw_preferences_group_add (ADW_PREFERENCES_GROUP (group), strength_row);
    }

    /* Whatever the prompt has to say, said once. The standing explanation
     * depends on which question is being asked; an error joins it rather than
     * replacing it, and the shell backend shows both the same way. */
    const gchar *standing = confirm
        ? g_paste_prompt_text (G_PASTE_PROMPT_TEXT_PASSPHRASE_CONFIRM_DESCRIPTION)
        : g_paste_prompt_text (G_PASTE_PROMPT_TEXT_PASSPHRASE_DESCRIPTION);
    g_autofree gchar *description = error_message ? g_strconcat (error_message, "\n\n", standing, NULL)
                                                  : NULL;

    adw_preferences_group_set_description (ADW_PREFERENCES_GROUP (group),
                                           description ? description : standing);

    /* No keyring in this build means nothing to offer to remember it in. */
    if (g_paste_prompt_keyring_available ())
    {
        GtkWidget *remember = adw_switch_row_new ();
        gboolean starts_on;

        self->remember = remember;
        adw_preferences_row_set_title (ADW_PREFERENCES_ROW (remember), g_paste_prompt_text (G_PASTE_PROMPT_TEXT_REMEMBER));
        adw_action_row_set_subtitle (ADW_ACTION_ROW (remember), g_paste_prompt_text (G_PASTE_PROMPT_TEXT_REMEMBER_SUBTITLE));

        g_paste_prompt_remember_state (g_paste_prompt_request_get_remember (request),
                                       &starts_on, &self->remembered);

        adw_switch_row_set_active (ADW_SWITCH_ROW (remember), starts_on);
        adw_preferences_group_add (ADW_PREFERENCES_GROUP (group), remember);
    }

    GtkWidget *page = adw_preferences_page_new ();

    adw_preferences_page_add (ADW_PREFERENCES_PAGE (page), ADW_PREFERENCES_GROUP (group));

    GtkWidget *toolbar = adw_toolbar_view_new ();

    adw_toolbar_view_add_top_bar (ADW_TOOLBAR_VIEW (toolbar), header);
    adw_toolbar_view_set_content (ADW_TOOLBAR_VIEW (toolbar), page);
    adw_application_window_set_content (ADW_APPLICATION_WINDOW (window), toolbar);

    g_signal_connect (window, "destroy", G_CALLBACK (passphrase_dialog_dismiss), self);
    g_object_set_data_full (G_OBJECT (window), "gpaste-passphrase-dialog", self, passphrase_dialog_free);

    GtkEventController *key_controller = gtk_event_controller_key_new ();

    g_signal_connect (key_controller, "key-pressed", G_CALLBACK (on_passphrase_key_pressed), self);
    gtk_widget_add_controller (window, key_controller);

    gtk_window_present (self->window);
}

/*
 * The report
 */

/* Escape closes it like its close button. Nothing is answered either way — a
 * report has no answer — so losing the window needs no teardown of its own. */
static gboolean
on_report_key_pressed (GtkEventControllerKey *controller G_GNUC_UNUSED,
                       guint                  keyval,
                       guint                  keycode    G_GNUC_UNUSED,
                       GdkModifierType        state      G_GNUC_UNUSED,
                       gpointer               user_data)
{
    if (keyval != GDK_KEY_Escape)
        return GDK_EVENT_PROPAGATE;

    gtk_window_close (user_data);
    return GDK_EVENT_STOP;
}

/* A window rather than an AdwAlertDialog, for the same reason the two prompts
 * above are windows: the daemon has no window to put a dialog inside. */
static void
g_paste_prompt_adw_report (GPastePrompt *prompt,
                           const gchar  *title,
                           const gchar  *message)
{
    GtkWidget *window = adw_application_window_new (G_PASTE_PROMPT_ADW (prompt)->application);

    gtk_window_set_title (GTK_WINDOW (window), title);
    gtk_window_set_icon_name (GTK_WINDOW (window), G_PASTE_ICON_NAME);
    gtk_window_set_default_size (GTK_WINDOW (window), 420, -1);
    gtk_window_set_modal (GTK_WINDOW (window), TRUE);

    GtkWidget *close = gtk_button_new_with_label (g_paste_prompt_text (G_PASTE_PROMPT_TEXT_CLOSE));

    gtk_widget_add_css_class (close, "suggested-action");
    g_signal_connect_swapped (close, "clicked", G_CALLBACK (gtk_window_destroy), window);

    GtkWidget *header = adw_header_bar_new ();

    adw_header_bar_pack_end (ADW_HEADER_BAR (header), close);

    GtkWidget *status = adw_status_page_new ();
    /* The description is Pango markup (the title is not), and @message carries a
     * GError's text: history names the user chose and paths from the filesystem.
     * One "&" in either makes the markup fail to parse, and the window would go
     * up with an empty description -- a report that reports nothing. */
    g_autofree gchar *escaped = g_markup_escape_text (message, -1);

    adw_status_page_set_icon_name (ADW_STATUS_PAGE (status), "dialog-warning-symbolic");
    adw_status_page_set_title (ADW_STATUS_PAGE (status), title);
    adw_status_page_set_description (ADW_STATUS_PAGE (status), escaped);

    GtkWidget *toolbar = adw_toolbar_view_new ();

    adw_toolbar_view_add_top_bar (ADW_TOOLBAR_VIEW (toolbar), header);
    adw_toolbar_view_set_content (ADW_TOOLBAR_VIEW (toolbar), status);
    adw_application_window_set_content (ADW_APPLICATION_WINDOW (window), toolbar);

    GtkEventController *key_controller = gtk_event_controller_key_new ();

    g_signal_connect (key_controller, "key-pressed", G_CALLBACK (on_report_key_pressed), window);
    gtk_widget_add_controller (window, key_controller);

    gtk_window_present (GTK_WINDOW (window));
}

static void
g_paste_prompt_adw_prompt_iface_init (GPastePromptInterface *iface)
{
    iface->passphrase = g_paste_prompt_adw_passphrase;
    iface->migration = g_paste_prompt_adw_migration;
    iface->report = g_paste_prompt_adw_report;
}

static void
g_paste_prompt_adw_class_init (GPastePromptAdwClass *klass G_GNUC_UNUSED)
{
}

static void
g_paste_prompt_adw_init (GPastePromptAdw *self G_GNUC_UNUSED)
{
}

/**
 * g_paste_prompt_adw_new:
 * @application: the #GtkApplication to anchor the dialogs to
 *
 * Create a new libadwaita #GPastePrompt backend.
 *
 * Returns: (transfer full): a newly allocated #GPastePrompt
 *          free it with g_object_unref
 */
GPastePrompt *
g_paste_prompt_adw_new (GtkApplication *application)
{
    g_return_val_if_fail (GTK_IS_APPLICATION (application), NULL);

    GPastePromptAdw *self = g_object_new (G_PASTE_TYPE_PROMPT_ADW, NULL);

    self->application = application;

    return G_PASTE_PROMPT (self);
}
