import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Atk from 'gi://Atk';
import GObject from 'gi://GObject';
import Pango from 'gi://Pango';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Animation from '../ui/animation.js';
import * as AuthList from './authList.js';
import * as Batch from './batch.js';
import {logErrorUnlessCancelled} from '../misc/errorUtils.js';
import * as Params from '../misc/params.js';
import * as ShellEntry from '../ui/shellEntry.js';
import * as UserVerifier from './userVerifier.js';
import * as UserWidget from '../ui/userWidget.js';
import * as WebLogin from './webLogin.js';
import {wiggle} from '../misc/animationUtils.js';

import {loadInterfaceXML} from '../misc/fileUtils.js';

const TimerChildIface = loadInterfaceXML('org.freedesktop.MalcontentTimer1.Child');
const TimerChildProxy = Gio.DBusProxy.makeProxyWrapper(TimerChildIface);

export const DEFAULT_BUTTON_WELL_ICON_SIZE = 16;
export const DEFAULT_BUTTON_WELL_ANIMATION_TIME = 300;
export const MESSAGE_FADE_OUT_ANIMATION_TIME = 500;

// A widget displayed instead of the unlock prompt
// when parental controls session limits are reached
const ParentalControlsShield = GObject.registerClass(
class ParentalControlsShield extends St.BoxLayout {
    _init() {
        super._init({
            style_class: 'parental-controls-shield',
            orientation: Clutter.Orientation.VERTICAL,
            x_align: Clutter.ActorAlign.CENTER,
        });

        this._requestExtensionCookie = null;

        this._timerChildProxy = new TimerChildProxy(Gio.DBus.system,
            'org.freedesktop.MalcontentTimer1',
            '/org/freedesktop/MalcontentTimer1',
            (proxy, error) => {
                if (error)
                    console.error(`Failed to get TimerChild proxy: ${error}`);
            },
            null, /* cancellable */
            Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION
        );

        this._timerChildProxy.connectSignal('ExtensionResponse', (proxy, sender, params) =>
            this._onExtensionResponse(proxy, sender, params));

        this.connect('destroy', this._onDestroy.bind(this));

        this._titleLabel = new St.Label({
            style_class: 'parental-controls-shield-title',
            text: _('Screen Time Limit Reached'),
        });
        this.add_child(this._titleLabel);

        this._descriptionLabel = new St.Label({
            style_class: 'parental-controls-shield-description',
            text: _('Daily limit for screen time on this device has been reached. Resume tomorrow.'),
        });
        this._descriptionLabel.clutter_text.line_wrap = true;
        this.add_child(this._descriptionLabel);

        this._ignoreButton = new St.Button({
            style_class: 'parental-controls-shield-button',
            // Translators: this is for ignoring a screen time limit for parental controls
            label: _('Ignore'),
            x_align: Clutter.ActorAlign.CENTER,
        });
        this._ignoreButton.connect('clicked',
            () => this._onIgnoreButtonClicked().catch(logError));
        this.add_child(this._ignoreButton);
    }

    _onDestroy() {
        this._requestExtensionCookie = null;
    }

    async _onIgnoreButtonClicked() {
        if (this._requestExtensionCookie)
            return;

        try {
            [this._requestExtensionCookie] = await this._timerChildProxy.RequestExtensionAsync(
                'login-session',
                '',
                0,
                {},
                Gio.DBusCallFlags.ALLOW_INTERACTIVE_AUTHORIZATION
            );
        } catch (e) {
            console.warn(`Failed to obtain screen time extension: ${e.message}`);
        }
    }

    _onExtensionResponse(proxy, sender, [_, cookie]) {
        if (this._requestExtensionCookie === null ||
            cookie !== this._requestExtensionCookie)
            return;

        this._requestExtensionCookie = null;
    }
});

/** @enum {number} */
export const AuthPromptMode = {
    UNLOCK_ONLY: 0,
    UNLOCK_OR_LOG_IN: 1,
};

/** @enum {number} */
export const AuthPromptStatus = {
    NOT_VERIFYING: 0,
    VERIFYING: 1,
    VERIFICATION_FAILED: 2,
    VERIFICATION_SUCCEEDED: 3,
    VERIFICATION_CANCELLED: 4,
    VERIFICATION_IN_PROGRESS: 5,
};

/** @enum {number} */
export const ResetType = {
    PROVIDE_USERNAME: 0,
    DONT_PROVIDE_USERNAME: 1,
    REUSE_USERNAME: 2,
};

export const AuthPrompt = GObject.registerClass({
    Signals: {
        'cancelled': {},
        'failed': {},
        'next': {},
        'prompted': {},
        'mechanisms-changed': {param_types: [GObject.TYPE_JSOBJECT]},
        'reset': {param_types: [GObject.TYPE_UINT]},
        'verification-complete': {},
        'loading': {param_types: [GObject.TYPE_BOOLEAN]},
    },
    Properties: {
        'verification-status': GObject.ParamSpec.uint(
            'verification-status', 'verification-status', 'verification-status',
            GObject.ParamFlags.READWRITE,
            AuthPromptStatus.NOT_VERIFYING, AuthPromptStatus.VERIFICATION_IN_PROGRESS, 0),
        'prompt-step': GObject.ParamSpec.uint(
            'prompt-step', 'prompt-step', 'prompt-step',
            GObject.ParamFlags.READWRITE,
            0, GLib.MAXUINT32, 0),
    },
}, class AuthPrompt extends St.BoxLayout {
    _init(gdmClient, mode) {
        super._init({
            style_class: 'login-dialog-prompt-layout',
            orientation: Clutter.Orientation.VERTICAL,
            x_expand: true,
            x_align: Clutter.ActorAlign.CENTER,
            reactive: true,
        });

        this.verificationStatus = AuthPromptStatus.NOT_VERIFYING;

        this._gdmClient = gdmClient;
        this._mode = mode;
        this._defaultButtonWellActor = null;
        this._cancelledRetries = 0;

        this.connect('notify::prompt-step', () => this._updateCancelButton());

        let reauthenticationOnly;
        if (this._mode === AuthPromptMode.UNLOCK_ONLY)
            reauthenticationOnly = true;
        else if (this._mode === AuthPromptMode.UNLOCK_OR_LOG_IN)
            reauthenticationOnly = false;

        this._userVerifier = this._createUserVerifier(this._gdmClient, {reauthenticationOnly});

        this._userVerifier.connectObject(
            'ask-question', (_, args) => this._onAskQuestion(args),
            'show-message', (_, args) => this._onShowMessage(args),
            'show-choice-list', (_, args) => this._onShowChoiceList(args),
            'show-button', (_, args) => this._onShowButton(args),
            'mechanisms-changed', (_, args) => this.emit('mechanisms-changed', args),
            'web-login', (_, args) => this._onWebLogin(args),
            'verification-failed', (_, args) => this._onVerificationFailed(args),
            'verification-complete', () => this._onVerificationComplete(),
            'reset', (_, args) => this._onReset(args),
            this);

        this.connect('destroy', this._onDestroy.bind(this));

        this._userWell = new St.Bin({
            x_expand: true,
            y_expand: true,
        });
        this.add_child(this._userWell);

        this._inputWell = new St.BoxLayout({
            style_class: 'login-dialog-input-well',
            orientation: Clutter.Orientation.VERTICAL,
        });
        this.add_child(this._inputWell);
        this._mainContent = this._inputWell;

        this._initInputRow();

        const capsLockPlaceholder = new St.Label();
        this._inputWell.add_child(capsLockPlaceholder);

        this._capsLockWarningLabel = new ShellEntry.CapsLockWarning({
            x_expand: true,
            x_align: Clutter.ActorAlign.CENTER,
        });
        this._inputWell.add_child(this._capsLockWarningLabel);

        this._capsLockWarningLabel.bind_property('visible',
            capsLockPlaceholder, 'visible',
            GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN);

        this._message = new St.Label({
            opacity: 0,
            styleClass: 'login-dialog-message',
            y_expand: true,
            x_expand: true,
            y_align: Clutter.ActorAlign.START,
            x_align: Clutter.ActorAlign.CENTER,
        });
        this._message.clutter_text.line_wrap = true;
        this._message.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
        this._inputWell.add_child(this._message);
    }

    _createUserVerifier(gdmClient, params) {
        return new UserVerifier.ShellUserVerifier(gdmClient, params);
    }

    _onDestroy() {
        this._inactiveEntry.destroy();
        this._inactiveEntry = null;
        this._userVerifier.disconnectObject(this);
        this._userVerifier.destroy();
        this._userVerifier = null;
        this._entry = null;
    }

    on_key_press_event(event) {
        if (event.get_key_symbol() === Clutter.KEY_Escape) {
            this._handleCancel();
            return Clutter.EVENT_STOP;
        }
        return Clutter.EVENT_PROPAGATE;
    }

    _initInputRow() {
        this._mainBox = new St.BoxLayout({
            style_class: 'login-dialog-button-box',
            orientation: Clutter.Orientation.HORIZONTAL,
        });
        this._inputWell.add_child(this._mainBox);

        this.cancelButton = new St.Button({
            style_class: 'login-dialog-button cancel-button',
            accessible_name: _('Back'),
            button_mask: St.ButtonMask.PRIMARY | St.ButtonMask.SECONDARY,
            reactive: true,
            can_focus: true,
            x_align: Clutter.ActorAlign.START,
            y_align: Clutter.ActorAlign.CENTER,
            icon_name: 'go-previous-symbolic',
        });
        this.cancelButton.connect('clicked', () => this._handleCancel());
        this._updateCancelButton();
        this._mainBox.add_child(this.cancelButton);

        this._authList = new AuthList.AuthList();
        this._authList.hide();
        this._authListActivateId = 0;
        this._inputWell.add_child(this._authList);

        this._authListTitle = new St.Bin({
            style_class: 'login-dialog-auth-list-title',
            x_expand: true,
            y_expand: true,
            child: new St.Label({style_class: 'login-dialog-auth-list-title-label'}),
            visible: false,
        });
        this._authList.bind_property('visible',
            this._authListTitle, 'visible',
            GObject.BindingFlags.DEFAULT);
        this._authList.bind_property('opacity',
            this._authListTitle, 'opacity',
            GObject.BindingFlags.DEFAULT);
        this._mainBox.add_child(this._authListTitle);

        this._authList.add_constraint(new Clutter.BindConstraint({
            coordinate: Clutter.BindCoordinate.WIDTH,
            source: this._authListTitle,
        }));
        this._authList.add_constraint(new Clutter.BindConstraint({
            coordinate: Clutter.BindCoordinate.X,
            source: this._authListTitle,
        }));

        this._entryArea = new St.Widget({
            style_class: 'login-dialog-prompt-entry-area',
            layout_manager: new Clutter.BinLayout(),
            x_expand: true,
            y_expand: true,
            visible: false,
        });
        this._mainBox.add_child(this._entryArea);

        const entryParams = {
            style_class: 'login-dialog-prompt-entry',
            can_focus: true,
            x_expand: true,
            y_expand: true,
        };

        this._entry = null;

        this._textEntry = new St.Entry(entryParams);
        ShellEntry.addContextMenu(this._textEntry, {actionMode: Shell.ActionMode.NONE});

        this._passwordEntry = new St.PasswordEntry(entryParams);
        ShellEntry.addContextMenu(this._passwordEntry, {actionMode: Shell.ActionMode.NONE});

        this._entry = this._passwordEntry;
        this._entryArea.add_child(this._entry);
        this._entry.grab_key_focus();
        this._inactiveEntry = this._textEntry;

        this._timedLoginIndicator = new St.Bin({
            style_class: 'login-dialog-timed-login-indicator',
            scale_x: 0,
        });

        this._inputWell.add_child(this._timedLoginIndicator);

        [this._textEntry, this._passwordEntry].forEach(entry => {
            entry.clutter_text.connect('text-changed', () => {
                if (!this._userVerifier.hasPendingMessages)
                    this._fadeOutMessage();
            });

            entry.clutter_text.connect('activate', () => this._activateNext());
        });

        this._defaultButtonWell = new St.Widget({
            layout_manager: new Clutter.BinLayout(),
            style_class: 'login-dialog-default-button-well',
            x_expand: true,
            x_align: Clutter.ActorAlign.END,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._entryArea.add_child(this._defaultButtonWell);

        this._nextButton = new St.Button({
            style_class: 'login-dialog-button next-button',
            accessible_name: _('Submit'),
            button_mask: St.ButtonMask.PRIMARY | St.ButtonMask.SECONDARY,
            reactive: true,
            can_focus: false,
            icon_name: 'go-next-symbolic',
        });
        this._nextButton.connect('clicked', () => this._activateNext());
        this._nextButton.add_style_pseudo_class('default');
        this._defaultButtonWell.add_child(this._nextButton);

        this._spinner = new Animation.Spinner(DEFAULT_BUTTON_WELL_ICON_SIZE);
        this._defaultButtonWell.add_child(this._spinner);

        this.setActorInDefaultButtonWell(this._nextButton);

        this._authButton = new St.Button({
            style_class: 'login-button',
            button_mask: St.ButtonMask.PRIMARY | St.ButtonMask.SECONDARY,
            can_focus: true,
            x_align: Clutter.ActorAlign.CENTER,
            x_expand: true,
            y_expand: true,
        });
        this._authButton.connect('clicked', () => this._completePendingCallback());
        this._mainBox.add_child(this._authButton);

        this._webLoginDialog = new WebLogin.WebLoginDialog();
        this._webLoginDialog.connect('cancel', () => this._handleCancel());
        this._webLoginDialog.connect('loading', () => this.emit('loading', this._webLoginDialog.isLoading));
        this._inputWell.add_child(this._webLoginDialog);

        // center elements inside _mainBox between the cancel
        // button on the left and this spacer on the right
        this._mainBox.add_child(new Clutter.Actor({
            constraints: new Clutter.BindConstraint({
                source: this.cancelButton,
                coordinate: Clutter.BindCoordinate.WIDTH,
            }),
        }));
    }

    _updateCancelButton() {
        if (this._mode === AuthPromptMode.UNLOCK_OR_LOG_IN)
            return;

        const cancelVisible = this.promptStep > 1;
        this.cancelButton.opacity = cancelVisible ? 255 : 0;
        this.cancelButton.reactive = cancelVisible;
    }

    showTimedLoginIndicator(time) {
        const hold = new Batch.Hold();

        this.hideTimedLoginIndicator();

        const startTime = GLib.get_monotonic_time();

        this._timedLoginTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 33,
            () => {
                const currentTime = GLib.get_monotonic_time();
                const elapsedTime = (currentTime - startTime) / GLib.USEC_PER_SEC;
                this._timedLoginIndicator.scale_x = elapsedTime / time;
                if (elapsedTime >= time) {
                    this._timedLoginTimeoutId = 0;
                    hold.release();
                    return GLib.SOURCE_REMOVE;
                }

                return GLib.SOURCE_CONTINUE;
            });

        GLib.Source.set_name_by_id(this._timedLoginTimeoutId, '[gnome-shell] this._timedLoginTimeoutId');

        return hold;
    }

    hideTimedLoginIndicator() {
        if (this._timedLoginTimeoutId) {
            GLib.source_remove(this._timedLoginTimeoutId);
            this._timedLoginTimeoutId = 0;
        }
        this._timedLoginIndicator.scale_x = 0.;
    }

    _activateNext() {
        if (!this._entry.reactive)
            return;

        this.verificationStatus = AuthPromptStatus.VERIFICATION_IN_PROGRESS;
        this.updateSensitivity({sensitive: false});

        if (this._entry === this._passwordEntry)
            this.startSpinning({animate: true});

        if (this._queryingService)
            this._completePendingCallback(this._entry.text);
        else
            this._preemptiveAnswer = this._entry.text;

        this._preemptiveInput = false;

        this.emit('next');
    }

    updateEntry(secret) {
        let newEntry, inactiveEntry;

        if (secret && this._entry !== this._passwordEntry) {
            newEntry = this._passwordEntry;
            inactiveEntry = this._textEntry;
        } else if (!secret && this._entry !== this._textEntry) {
            newEntry = this._textEntry;
            inactiveEntry = this._passwordEntry;
        }

        if (newEntry) {
            this._entryArea.replace_child(this._entry, newEntry);
            this._entry = newEntry;
            this._inactiveEntry = inactiveEntry;

            const {text, cursorPosition, selectionBound} = inactiveEntry.clutterText;
            this._entry.clutterText.set({text, cursorPosition, selectionBound});
        }

        this._capsLockWarningLabel.visible = secret;
    }

    _setPendingCallback(callback) {
        if (this._pendingCallback)
            throw new Error('A pending request is already active');
        this._pendingCallback = callback;
    }

    _completePendingCallback(...args) {
        if (!this._pendingCallback)
            throw new Error('No pending request to complete');

        const callback = this._pendingCallback;
        this._pendingCallback = null;

        this._userVerifier.handlePendingMessages()
            .then(() => callback(...args))
            .catch(logErrorUnlessCancelled);
    }

    _onAskQuestion({serviceName, question, secret, answerHandler}) {
        if (this._queryingService)
            this.clear();

        this._queryingService = serviceName;
        this.promptStep++;

        this._setPendingCallback(answerHandler);

        const preemptiveAnswer = this._preemptiveAnswer;
        this._clearPreemptiveState();
        if (preemptiveAnswer) {
            this._completePendingCallback(preemptiveAnswer);
            return;
        }

        this.updateEntry(secret);

        // Hack: The question string comes directly from PAM, if it's "Password:"
        // we replace it with our own to allow localization, if it's something
        // else we remove the last colon and any trailing or leading spaces.
        if (question === 'Password:' || question === 'Password: ')
            this.setQuestion(_('Password'));
        else
            this.setQuestion(question.replace(/[:：] *$/, '').trim());

        this.emit('prompted');
    }

    _onShowChoiceList({serviceName, promptMessage, choiceList, choiceHandler}) {
        if (this._queryingService)
            this.clear();

        this._queryingService = serviceName;
        this.promptStep++;

        this._clearPreemptiveState();

        this._connectAuthListActivate();
        this._setPendingCallback(choiceHandler);

        this.setChoiceList(promptMessage, choiceList);
        this.updateSensitivity({sensitive: true});
        this.emit('prompted');
    }

    _onShowMessage({message, type, shouldWiggle, showMessageResolver}) {
        this.setMessage(message, type);
        this.emit('prompted');

        // If we're showing a message and no auth widget is currently visible,
        // show the entry area to allow getting a preemptive answer
        if (message &&
            type < UserVerifier.MessageType.ERROR &&
            !this._entryArea.visible &&
            !this._authList.visible &&
            !this._authButton.visible &&
            !this._webLoginDialog.visible) {
            this._fadeInElement(this._entryArea);
            this.updateSensitivity({sensitive: true});
        }

        const wigglePromise = shouldWiggle
            ? wiggle(this._message, {duration: 65, wiggleCount: 3})
            : Promise.resolve();

        showMessageResolver?.(wigglePromise);
    }

    _onShowButton({serviceName, label, callback}) {
        if (this._queryingService)
            this.clear();

        this._queryingService = serviceName;
        this.promptStep++;

        this._clearPreemptiveState();

        this._setPendingCallback(callback);

        this._authButton.set_label(label);

        this._fadeInElement(this._authButton);
        this.updateSensitivity({sensitive: true});
        this.emit('prompted');
    }

    _onWebLogin({serviceName, message, url, code, buttons}) {
        if (this._queryingService)
            this.clear();

        this._queryingService = serviceName;
        this.promptStep++;

        this._webLoginParams = {message, url, code, buttons};

        this._entryArea.hide();

        this._clearPreemptiveState();

        this._openWebLoginDialog();

        this.emit('prompted');
    }

    _closeWebLoginDialog() {
        this._webLoginDialog.hide();

        this._userWell.get_child()?.showAvatar();
        this._mainBox.show();

        this.webLoginActive = false;
        this.remove_style_class_name('web-login-active');
    }

    _openWebLoginDialog() {
        this._userWell.get_child()?.hideAvatar();
        this._mainBox.hide();

        this._webLoginDialog.update(this._webLoginParams);
        this._fadeInElement(this._webLoginDialog);
        this.updateSensitivity({sensitive: true});

        this.webLoginActive = true;
        this.add_style_class_name('web-login-active');
    }

    _onVerificationFailed({serviceName, canRetry}) {
        const wasQueryingService = this._queryingService === serviceName;

        if (wasQueryingService)
            this._queryingService = null;

        // Only allow instant retrying with password authentication.
        // The rest of authentications will retry through the reset flow.
        if (canRetry && this._userVerifier.selectedMechanism?.preemptiveInput) {
            this.verificationStatus = AuthPromptStatus.VERIFYING;
            this._entry.text = '';
            this.startPreemptiveInput();
        }
        this.stopSpinning();

        if (!canRetry)
            this.verificationStatus = AuthPromptStatus.VERIFICATION_FAILED;

        if (wasQueryingService)
            wiggle(this._entryArea);
    }

    _onVerificationComplete() {
        this.stopSpinning({animate: true});
        this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED;

        [this._mainBox, this._webLoginDialog].forEach(widget => {
            widget.reactive = false;
            widget.ease({
                opacity: 0,
                duration: MESSAGE_FADE_OUT_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
        });

        this.emit('verification-complete');
    }

    _onReset(resetParams) {
        if (this.verificationStatus === AuthPromptStatus.VERIFICATION_SUCCEEDED)
            return;

        this.reset(resetParams);
    }

    setActorInDefaultButtonWell(actor, animate) {
        if (!this._defaultButtonWellActor && !actor)
            return;

        const oldActor = this._defaultButtonWellActor;
        const wasSpinner = oldActor === this._spinner;

        if (oldActor)
            oldActor.remove_all_transitions();

        if (actor === this._spinner)
            this._spinner.play();

        if (!animate) {
            if (oldActor) {
                oldActor.opacity = 0;
                if (wasSpinner)
                    this._spinner.stop();
            }
            if (actor)
                actor.opacity = 255;

            this._defaultButtonWellActor = actor;
            return;
        }

        if (oldActor) {
            oldActor.opacity = 255;
            oldActor.ease({
                opacity: 0,
                duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME,
                mode: Clutter.AnimationMode.LINEAR,
                onComplete: () => {
                    if (wasSpinner)
                        this._spinner.stop();
                },
            });
        }
        if (actor) {
            actor.opacity = 0;
            actor.ease({
                opacity: 255,
                duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME,
                delay: oldActor ? DEFAULT_BUTTON_WELL_ANIMATION_TIME : 0,
                mode: Clutter.AnimationMode.LINEAR,
            });
        }

        this._defaultButtonWellActor = actor;
    }

    startSpinning({animate = false} = {}) {
        this.emit('loading', true);
        this.setActorInDefaultButtonWell(this._spinner, animate);
    }

    stopSpinning({animate = false} = {}) {
        this.emit('loading', false);
        this.setActorInDefaultButtonWell(this._nextButton, animate);

        if (this._webLoginDialog.isLoading)
            this._webLoginDialog.stopLoading();
    }

    clear(params) {
        const {reuseEntryText} = Params.parse(params, {
            reuseEntryText: false,
        });

        if (!reuseEntryText) {
            this._entry.hint_text = '';
            this._inactiveEntry.hint_text = '';
            this._entryArea.hide();
            this._entry.text = '';
            this._inactiveEntry.text = '';
            this.stopSpinning();
        }

        this._authListTitle.child.text = '';
        this._authList.clear();
        this._authList.hide();
        this._disconnectAuthListActivate();
        this._authButton.hide();
        this._closeWebLoginDialog();
        this._pendingCallback = null;

        [this._mainBox, this._webLoginDialog].forEach(widget => {
            widget.opacity = 255;
            widget.reactive = true;
        });
    }

    setQuestion(question) {
        this._entry.hint_text = question;

        this._authList.hide();
        this._authButton.hide();
        this._closeWebLoginDialog();

        this._fadeInElement(this._entryArea);
        this.updateSensitivity({sensitive: true});
    }

    _connectAuthListActivate() {
        if (this._authListActivateId)
            return;

        this._authListActivateId =
            this._authList.connect('activate', (list, key) => {
                this._authList.reactive = false;
                this._authList.ease({
                    opacity: 0,
                    duration: MESSAGE_FADE_OUT_ANIMATION_TIME * 0.5,
                    mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                    onComplete: () => {
                        this._authListTitle.child.text = '';
                        this._authList.clear();
                        this._authList.hide();
                        this._completePendingCallback(key);
                    },
                });
            });
    }

    _disconnectAuthListActivate() {
        if (this._authListActivateId) {
            this._authList.disconnect(this._authListActivateId);
            this._authListActivateId = 0;
        }
    }

    _fadeInElement(element) {
        if (element.visible)
            return;

        element.set({
            opacity: 0,
            visible: true,
        });
        element.ease({
            opacity: 255,
            duration: MESSAGE_FADE_OUT_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    setChoiceList(promptMessage, choiceList) {
        this._authList.clear();
        this._authListTitle.child.text = promptMessage;
        for (const key in choiceList) {
            const content = choiceList[key];
            this._authList.addItem(key, content);
        }

        this._entryArea.hide();
        this._fadeInElement(this._authList);
        this.updateSensitivity({sensitive: true});
    }

    getAnswer() {
        let text;

        if (this._preemptiveAnswer) {
            text = this._preemptiveAnswer;
            this._preemptiveAnswer = null;
        } else {
            text = this._entry.get_text();
        }

        return text;
    }

    _fadeOutMessage() {
        if (this._message.opacity === 0)
            return;
        this._message.remove_all_transitions();
        this._message.ease({
            opacity: 0,
            duration: MESSAGE_FADE_OUT_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    setMessage(message, type) {
        if (type === UserVerifier.MessageType.ERROR)
            this._message.add_style_class_name('login-dialog-message-warning');
        else
            this._message.remove_style_class_name('login-dialog-message-warning');

        if (type === UserVerifier.MessageType.HINT)
            this._message.add_style_class_name('login-dialog-message-hint');
        else
            this._message.remove_style_class_name('login-dialog-message-hint');

        if (message) {
            this._message.remove_all_transitions();
            this._message.text = message;
            this._message.opacity = 255;
            this.get_accessible().emit('notification', message, Atk.Live.ASSERTIVE);
        } else {
            this._message.opacity = 0;
        }
    }

    updateSensitivity({sensitive}) {
        if (sensitive && this._preemptiveAnswer)
            return;

        const authWidget = [
            this._authList,
            this._authButton,
            this._webLoginDialog,
        ].find(widget => widget.visible) ?? this._entry;

        if (authWidget === this._entry)
            this._nextButton.reactive = sensitive;

        authWidget.reactive = sensitive;

        if (sensitive) {
            authWidget.grab_key_focus();
        } else {
            this.grab_key_focus();

            if (authWidget === this._passwordEntry)
                authWidget.password_visible = false;
        }
    }

    vfunc_hide() {
        this.stopSpinning();
        super.vfunc_hide();
        this._message.opacity = 0;

        this.setUser(null);

        this.updateSensitivity({sensitive: true});
        this._entry.set_text('');
    }

    setUser(user) {
        const oldChild = this._userWell.get_child();
        if (oldChild)
            oldChild.destroy();

        const userWidget = new UserWidget.UserWidget(user, Clutter.Orientation.VERTICAL);
        this._userWell.set_child(userWidget);
    }

    selectMechanism(mechanism) {
        const invalidStatus = [
            AuthPromptStatus.VERIFICATION_SUCCEEDED,
            AuthPromptStatus.VERIFICATION_IN_PROGRESS,
        ];
        if (invalidStatus.includes(this.verificationStatus))
            return false;

        const oldPromptStep = this.promptStep;
        this.promptStep = 0;
        if (!this._userVerifier.selectMechanism(mechanism))
            this.promptStep = oldPromptStep;

        return true;
    }

    reset(params) {
        let {reuseEntryText, softReset} = Params.parse(params, {
            reuseEntryText: false,
            softReset: false,
        });

        const oldStatus = this.verificationStatus;
        this.verificationStatus = AuthPromptStatus.NOT_VERIFYING;
        if (oldStatus !== AuthPromptStatus.VERIFICATION_IN_PROGRESS)
            this._preemptiveAnswer = null;
        this.promptStep = 0;

        if (softReset)
            this._userVerifier?.cancel();
        else
            this._userVerifier?.reset();

        reuseEntryText = reuseEntryText || !!this._preemptiveAnswer || this._preemptiveInput;

        this._queryingService = null;
        this.clear({reuseEntryText});
        this._message.opacity = 0;
        this.setUser(null);
        this.updateEntry(true);

        if (oldStatus === AuthPromptStatus.VERIFICATION_FAILED)
            this.emit('failed');
        else if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED)
            this.emit('cancelled');

        let resetType;

        if (this._mode === AuthPromptMode.UNLOCK_ONLY) {
            // The user is constant at the unlock screen, so it will immediately
            // respond to the request with the username
            if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED)
                return;
            resetType = ResetType.PROVIDE_USERNAME;
        } else if (!this._userVerifier.needsUsername()) {
            // We don't need to know the username if the user preempted the login screen
            // with a smartcard or with preauthenticated oVirt credentials
            resetType = ResetType.DONT_PROVIDE_USERNAME;
        } else if (oldStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS ||
            softReset) {
            // We're going back to retry with current user
            resetType = ResetType.REUSE_USERNAME;
        } else {
            // In all other cases, we should get the username up front.
            resetType = ResetType.PROVIDE_USERNAME;
        }

        this.emit('reset', resetType);
    }

    startPreemptiveInput(unichar) {
        this._preemptiveInput = true;
        this.updateSensitivity({sensitive: true});
        if (unichar)
            this._entry.clutter_text.insert_unichar(unichar);
    }

    _clearPreemptiveState() {
        this._preemptiveInput = false;
        this._preemptiveAnswer = null;
    }

    /*
     * Set whether to block the authentication with the parental controls shield.
     *
     * @param {boolean} shouldBlock Whether to block the authentication
     */
    setAuthBlocked(shouldBlock) {
        if (!this._parentalControlsShield)
            this._parentalControlsShield = new ParentalControlsShield();

        const newMainContent = shouldBlock
            ? this._parentalControlsShield
            : this._inputWell;

        if (newMainContent !== this._mainContent) {
            this.replace_child(this._mainContent, newMainContent);
            this._mainContent = newMainContent;
        }

        if (this._mainContent === this._inputWell)
            this._entry.grab_key_focus();
    }

    begin(params) {
        params = Params.parse(params, {
            userName: null,
            hold: null,
        });

        if (!this._preemptiveInput)
            this.updateSensitivity({sensitive: false});

        this._userVerifier.begin(params.userName, params.hold).catch(
            logErrorUnlessCancelled);
        this.verificationStatus = AuthPromptStatus.VERIFYING;
    }

    finish(onComplete) {
        if (!this._userVerifier.hasPendingMessages) {
            this._userVerifier.clear();
            onComplete();
            return;
        }

        const signalId = this._userVerifier.connect('no-more-messages', () => {
            this._userVerifier.disconnect(signalId);
            this._userVerifier.clear();
            onComplete();
        });
    }

    _handleCancel() {
        if (this._userVerifier.cancelRequested()) {
            // We substract 2 because on cancel we'll receive a new prompt signal
            // which increments promptStep, so that ends with a result of going
            // back one step.
            this.promptStep -= 2;
            if (this.promptStep < 0)
                this.promptStep = 0;
            return;
        }

        this.cancel();
    }

    cancel() {
        if (this.verificationStatus === AuthPromptStatus.VERIFICATION_SUCCEEDED)
            return;

        // If we're in a multi-step flow (step > 1), go back to step 1 instead of full reset
        if (this.promptStep > 1) {
            this.reset({softReset: true});
            return;
        }

        if (this.verificationStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS) {
            this._cancelledRetries++;
            if (this._cancelledRetries > this._userVerifier.allowedFailures)
                this.verificationStatus = AuthPromptStatus.VERIFICATION_FAILED;
        } else {
            this.verificationStatus = AuthPromptStatus.VERIFICATION_CANCELLED;
        }

        this.reset();
    }
});
