window.app = window.app || {};
window.app.modals = window.app.modals || {};

/**
 * Opens a record upsert modal.
 *
 * @example
 * ```js
 * // create
 * app.modals.openRecordUpsert(collection)
 *
 * // update
 * app.modals.openRecordUpsert(collection, record)
 * ```
 *
 * @param {Object} collection
 * @param {Object} [record]
 * @param {Object} [modalSettings]
 */
window.app.modals.openRecordUpsert = function(collection, record = null, modalSettings = {
    // base modal events
    onbeforeopen: null, // function(el) {},
    onafteropen: null, // function(el) {},
    onbeforeclose: null, // function(el) {},
    onafterclose: null, // function(el) {},
    // record specific events
    onsave: null, // function(record, isNew) {},
    ondelete: null, // function(record) {},
    onduplicate: null, // function(record) {},
    ontokensreset: null, // function(record) {},
    onpasswordresetsend: null, // function(record) {},
    onverificationsend: null, // function(record) {},
}) {
    app.store.errors = null; // reset

    const modal = recordUpsertModal(collection, record, modalSettings);
    if (!modal) {
        return;
    }

    document.body.appendChild(modal);

    app.modals.open(modal);
};

const TAB_MAIN = "main";
const TAB_AUTH_PROVIDERS = "authProviders";

// @todo consider exporting the "tabs" with the final version
function recordUpsertModal(collection, rawRecord, modalSettings) {
    if (!collection?.id) {
        console.warn("[recordUpsertModal] missing required collection");
        return;
    }

    let modal;

    const uniqueId = "record_upsert_" + app.utils.randomString();

    const listingColumnsPreferences = app.utils.getLocalHistory(app.consts.COLUMNS_STORAGE_PREFIX + collection.id, {});

    const data = store({
        isLoading: true,
        isSaving: false,
        isLocked: false,
        originalRecord: {},
        record: {},
        initialDraft: null,
        activeTab: TAB_MAIN,
        get isNew() {
            return app.utils.isEmpty(data.originalRecord?.id);
        },
        get isAuthCollection() {
            return collection.type == "auth";
        },
        get isSuperusersCollection() {
            return collection.name == "_superusers";
        },
        get showTabs() {
            return !data.isNew && data.isAuthCollection && !data.isSuperusersCollection;
        },
        get excludedFields() {
            const result = ["id"];

            if (data.isAuthCollection) {
                result.push("email", "emailVisibility", "verified", "password", "tokenKey");
            }

            return result;
        },
        get initialDraftHash() {
            return serializeRecord(data.initialDraft);
        },
        get recordHash() {
            return serializeRecord(data.record);
        },
        get originalRecordHash() {
            return serializeRecord(data.originalRecord);
        },
        get hasChanges() {
            return (
                data.originalRecordHash != data.recordHash
                // manually check the password fields as they are out of the serialized hash
                || (
                    data.isAuthCollection
                    && (!!data.record.password || !!data.record.passwordConfirm)
                )
            );
        },
        get isFormDisabled() {
            return data.isLoading || data.isSaving || (!data.isNew && !data.hasChanges);
        },
    });

    // --- serialization helpers:

    // redact common sensitive fields
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter
    function redactedReplacer(key, val) {
        switch (key) {
            case "expand":
                return undefined;
            // exclude internal auth record sensitive fields
            case "password":
            case "passwordConfirm":
            case "tokenKey":
                return data.isAuthCollection ? undefined : val;
        }

        return val;
    }

    function serializeRecord(record) {
        if (!record) {
            return "";
        }

        return JSON.stringify(record, redactedReplacer);
    }

    // ---

    // note: not a getter to avoid the microtask batching
    function draftKey() {
        return "draft_" + collection.id + "_" + (data.originalRecord?.id || "");
    }

    function getDraftHash() {
        return window.localStorage.getItem(draftKey()) || "";
    }

    function getDraft() {
        try {
            const raw = getDraftHash();
            if (raw) {
                return JSON.parse(raw);
            }
        } catch (err) {
            console.warn("getDraft failure:", err);
            deleteDraft();
        }

        return null;
    }

    function saveDraft(serializedJSON) {
        try {
            window.localStorage.setItem(draftKey(), serializedJSON);
        } catch (e) {
            // ignore local storage errors in case the serialized data
            // exceed the browser localStorage single value quota
            console.warn("saveDraft failure:", e);
            deleteDraft();
        }
    }

    function deleteDraft() {
        window.localStorage.removeItem(draftKey());
        data.initialDraft = null;
    }

    function restoreDraft() {
        if (!data.initialDraft) {
            return;
        }

        if (app.store.errors) {
            app.store.errors = null;
        }

        const draftClone = JSON.parse(JSON.stringify(data.initialDraft));

        deleteDraft();

        data.record = draftClone;
    }

    let draftWatcher;

    function initDraftWatcher() {
        data.initialDraft = getDraft();

        draftWatcher?.unwatch();
        draftWatcher = watch(() => data.recordHash, (newHash, oldHash) => {
            if (typeof oldHash == "undefined") {
                return;
            }

            if (data.hasChanges) {
                saveDraft(newHash);
            } else {
                deleteDraft();
            }
        });
    }

    async function initRecord(rawRecord) {
        data.isLoading = true;

        draftWatcher?.unwatch();

        // normalize rawRecord (could be plain id string)
        rawRecord = app.utils.isObject(rawRecord) ? rawRecord : { id: rawRecord || "" };

        // new record
        if (!rawRecord.id) {
            data.originalRecord = JSON.parse(JSON.stringify(rawRecord));
            data.record = JSON.parse(JSON.stringify(rawRecord));

            data.isLoading = false;
            data.isLocked = false;
            initDraftWatcher();
            return;
        }

        try {
            data.isLocked = !!app.store.settings?.meta?.hideControls;

            // preload to minimize content jumps
            data.originalRecord = JSON.parse(JSON.stringify(rawRecord));
            data.record = JSON.parse(JSON.stringify(rawRecord));

            // fetch to ensure that the main record fields are up-to-date
            let record = await app.pb.collection(collection.name).getOne(rawRecord.id, {
                requestKey: "upsert_load_" + rawRecord.id,
            });

            // preload existing expands (if any)
            if (rawRecord.expand) {
                record.expand = JSON.parse(JSON.stringify(rawRecord.expand));
            }

            // extend, not overwrite, to prevent reseting the reference passed down to the inputs
            Object.assign(data.originalRecord, JSON.parse(JSON.stringify(record)));
            Object.assign(data.record, JSON.parse(JSON.stringify(record)));

            data.isLoading = false;

            // schedule a macro task to allow fields to populate their reactive values
            setTimeout(() => {
                initDraftWatcher();
            }, 0);
        } catch (err) {
            if (!err?.isAbort) {
                app.checkApiError(err);
                data.isLoading = false;
                setTimeout(() => app.modals.close(modal, true), 0);
            }
        }
    }

    function deleteInternalKeys(record) {
        for (let key in record) {
            if (
                key.startsWith("@@")
                || (data.isAuthCollection && (key == "password" || key == "passwordConfirm"))
            ) {
                delete record[key];
            }
        }
    }

    async function exportPayload() {
        const payload = {};

        // shallow copy of the record fields
        for (const key in data.record) {
            // skip expand and internal dynamic enumerable props
            if (key == "expand" || key.startsWith("@@")) {
                continue;
            }

            let val = data.record[key]?.__raw || data.record[key];

            // normalize undefined values
            if (typeof val == "undefined") {
                val = null;
            }

            payload[key] = val;
        }

        // apply fields save normalization funcs
        for (const field of collection.fields) {
            const saveHook = app.fieldTypes[field.type]?.onrecordsave;
            if (!saveHook) {
                continue;
            }

            await saveHook({
                collection: collection,
                originalRecord: data.originalRecord,
                record: data.record,
                field: field,
                payload: payload,
            });
        }

        return payload;
    }

    async function save(close = true) {
        if (data.isLocked || data.isSaving || (!data.isNew && !data.hasChanges)) {
            return;
        }

        data.isSaving = true;

        try {
            const payload = await exportPayload();

            const isNew = app.utils.isEmpty(data.originalRecord?.id);

            let record;
            if (isNew) {
                record = await app.pb.collection(collection.name).create(payload);
            } else {
                record = await app.pb.collection(collection.name).update(data.originalRecord.id, payload);
            }

            deleteDraft();

            if (isNew) {
                // replace to ensure the same keys order and to force inputs rerender
                data.originalRecord = structuredClone(record);
                data.record = structuredClone(record);
            } else {
                // extend, not overwrite, to prevent reseting the reference passed down to the inputs
                Object.assign(data.originalRecord, structuredClone(record));
                Object.assign(data.record, structuredClone(record));
                deleteInternalKeys(data.originalRecord);
                deleteInternalKeys(data.record);
            }

            modalSettings.onsave?.(record, isNew);

            // reset all errors
            app.store.errors = null;

            let msg;
            if (isNew) {
                msg = `Successfully created ${collection.name} "${record.id}".`;
            } else {
                msg = `Successfully updated ${collection.name} "${record.id}".`;
            }
            app.toasts.success(msg, { key: "recordSave" });

            data.isSaving = false;

            if (close) {
                app.modals.close(modal, true);
            }
        } catch (err) {
            if (!err?.isAbort) {
                data.isSaving = false;
                app.checkApiError(err, false);
                app.toasts.error(err.message || "Failed to save record.", { key: "recordSave" });
            }
        }
    }

    function resetForm() {
        deleteDraft();
        data.record = JSON.parse(JSON.stringify(data.originalRecord));
    }

    async function duplicate() {
        const clone = data.originalRecord ? JSON.parse(JSON.stringify(data.originalRecord)) : {};
        clone.id = "";

        // apply fields duplicate hook
        for (const field of collection.fields) {
            const duplicateHook = app.fieldTypes[field.type]?.onrecordduplicate;
            if (!duplicateHook) {
                continue;
            }

            await duplicateHook({
                collection: collection,
                field: field,
                originalRecord: data.originalRecord,
                clone: clone,
            });
        }

        deleteDraft();

        modalSettings.onduplicate?.(clone);

        initRecord(clone);
    }

    function mainTab() {
        return [
            t.div(
                { className: "modal-content" },
                t.div(
                    {
                        id: uniqueId + "form",
                        className: "grid",
                        inert: () => data.isLoading || data.isSaving,
                        onmount: (el) => {
                            el._quickSaveHandler = (e) => {
                                if ((e.ctrlKey || e.metaKey) && e.code == "KeyS") {
                                    e.preventDefault();
                                    save(false);
                                }
                            };
                            window.addEventListener("keydown", el._quickSaveHandler);
                        },
                        onunmount: (el) => {
                            if (el?._quickSaveHandler) {
                                window.removeEventListener("keydown", el?._quickSaveHandler);
                            }
                        },
                    },
                    // draft alert
                    () => {
                        if (
                            data.isLoading
                            || data.hasChanges
                            || app.utils.isEmpty(data.initialDraft)
                            || data.initialDraftHash == data.recordHash
                        ) {
                            return;
                        }

                        return t.div(
                            { className: "col-12" },
                            t.div(
                                { className: "alert warning flex gap-sm" },
                                t.div({ className: "content" }, "The record has previous unsaved changes."),
                                t.button(
                                    {
                                        type: "button",
                                        className: "btn sm outline",
                                        onclick: () => restoreDraft(),
                                    },
                                    t.span({ className: "txt" }, "Restore draft"),
                                ),
                                t.button(
                                    {
                                        type: "button",
                                        className: "btn sm secondary transparent circle m-l-auto",
                                        ariaLabel: app.attrs.tooltip("Discard draft", "left"),
                                        onclick: () => {
                                            deleteDraft();
                                        },
                                    },
                                    t.i({ className: "ri-close-line", ariaHidden: true }),
                                ),
                            ),
                        );
                    },
                    // primary key
                    () => {
                        const pkField = collection.fields?.find((f) => f.primaryKey);

                        return t.div(
                            { className: "col-12" },
                            app.fieldTypes[pkField.type].input({
                                get collection() {
                                    return collection;
                                },
                                get originalRecord() {
                                    return data.originalRecord;
                                },
                                get record() {
                                    return data.record;
                                },
                                get field() {
                                    return pkField;
                                },
                            }),
                        );
                    },
                    // special collection fields
                    () => {
                        if (!data.isAuthCollection) {
                            return;
                        }

                        const result = [
                            t.div({ className: "col-12" }, authFieldEmail(collection, data)),
                            t.div({ className: "col-12" }, authFieldPassword(collection, data)),
                        ];

                        // superusers are always verified
                        if (!data.isSuperusersCollection) {
                            result.push(
                                t.div({ className: "col-12" }, authFieldVerified(collection, data)),
                            );
                        }

                        return result;
                    },
                    // regular collection fields
                    () => {
                        const rows = [];

                        const excludedFields = data.excludedFields;

                        for (const field of collection.fields) {
                            if (!app.fieldTypes[field.type]?.input || excludedFields.includes(field.name)) {
                                continue;
                            }

                            rows.push(
                                t.div(
                                    // blur if not hidden and not explicitly toggle-on
                                    {
                                        className: () =>
                                            `col-12 ${
                                                field.hidden && !listingColumnsPreferences[field.id]
                                                    ? "hidden-field-blur"
                                                    : ""
                                            }`,
                                    },
                                    () => {
                                        return app.fieldTypes[field.type].input({
                                            get collection() {
                                                return collection;
                                            },
                                            get originalRecord() {
                                                return data.originalRecord;
                                            },
                                            get record() {
                                                return data.record;
                                            },
                                            get field() {
                                                return field;
                                            },
                                        });
                                    },
                                ),
                            );
                        }

                        if (rows.length && data.isAuthCollection) {
                            rows.unshift(t.div({ className: "col-12" }, t.hr({ className: "m-0" })));
                        }

                        return rows;
                    },
                ),
            ),
            t.footer(
                { className: "modal-footer" },
                t.button(
                    {
                        type: "button",
                        className: "btn transparent m-r-auto",
                        disabled: () => data.isSaving,
                        onclick: () => app.modals.close(modal),
                    },
                    t.span({ className: "txt" }, "Close"),
                ),
                t.button(
                    {
                        hidden: () => !data.isLocked,
                        type: "button",
                        className: "btn outline",
                        disabled: () => data.isFormDisabled,
                        onclick: () => data.isLocked = false,
                    },
                    t.i({ className: "ri-lock-unlock-line", ariaHidden: true }),
                    t.span({ className: "txt" }, "Unlock to save"),
                ),
                t.div(
                    {
                        hidden: () => data.isLocked,
                        className: "btns",
                    },
                    t.button(
                        {
                            type: "button",
                            className: () => `btn expanded-lg ${data.isLoading || data.isSaving ? "loading" : ""}`,
                            disabled: () => data.isLocked || data.isFormDisabled,
                            onclick: () => save(),
                        },
                        t.span({ className: "txt" }, () => (data.isNew ? "Create" : "Save changes")),
                    ),
                    t.button(
                        {
                            type: "button",
                            className: () => `btn p-5`,
                            title: "Save options",
                            disabled: () => data.isLocked || data.isFormDisabled,
                            "html-popovertarget": uniqueId + "save_options",
                        },
                        t.i({ className: "ri-arrow-up-s-line", ariaHidden: true }),
                    ),
                    t.div(
                        { id: uniqueId + "save_options", className: "dropdown nowrap", popover: "auto" },
                        t.button(
                            {
                                type: "button",
                                className: "dropdown-item",
                                onclick: (e) => {
                                    e.target.closest(".dropdown").hidePopover();
                                    save(false);
                                },
                            },
                            t.span({ className: "txt" }, "Save and continue"),
                            t.small({ className: "txt-hint" }, "(Ctrl+S)"),
                        ),
                        t.hr(),
                        t.button(
                            {
                                type: "button",
                                className: "dropdown-item",
                                onclick: (e) => {
                                    e.target.closest(".dropdown").hidePopover();
                                    resetForm();
                                },
                            },
                            t.span({ className: "txt" }, "Reset form"),
                        ),
                    ),
                ),
            ),
        ];
    }

    modal = t.div(
        {
            pbEvent: "recordUpsertModal",
            className: "modal record-upsert-modal",
            onbeforeopen: () => {
                initRecord(rawRecord);

                return modalSettings.onbeforeopen?.(el);
            },
            onafteropen: (el) => {
                modalSettings.onafteropen?.(el);
            },
            onbeforeclose: (el, forceClosed) => {
                if (forceClosed) {
                    return modalSettings.onbeforeclose?.(el);
                }

                if (data.isLoading || data.isSaving) {
                    return false;
                }

                if (!data.hasChanges) {
                    return modalSettings.onbeforeclose?.(el);
                }

                return new Promise((r) => {
                    app.modals.confirm(
                        "You have unsaved changes. Do you really want to discard them?",
                        () => {
                            deleteDraft();
                            return r(modalSettings.onbeforeclose?.(el));
                        },
                        () => r(false),
                        { yesButton: "Yes, discard" },
                    );
                });
            },
            onafterclose: (el) => {
                modalSettings.onafterclose?.(el);
                el?.remove();
            },
            onunmount: () => {
                draftWatcher?.unwatch();
            },
        },
        t.header(
            { className: () => `modal-header ${data.showTabs ? "isolated" : ""}` },
            t.div(
                { className: "grid" },
                t.div(
                    { className: "col-12 flex" },
                    t.h6(
                        { className: "modal-title" },
                        t.span(null, () => (data.isNew ? "Create " : "Edit ")),
                        t.strong(
                            { className: "txt-ellipsis collection-name", style: "max-width: 220px" },
                            () => collection.name,
                        ),
                        t.span(null, " record"),
                    ),
                    t.div({ className: "flex-fill" }),
                    () => {
                        if (app.utils.isEmpty(data.originalRecord?.id)) {
                            return;
                        }

                        return [
                            t.button(
                                {
                                    type: "button",
                                    title: "More options",
                                    className: () => `btn sm circle transparent ${data.isLoading ? "loading" : ""}`,
                                    disabled: () => data.isLoading,
                                    "html-popovertarget": uniqueId + "modal-header-dropdown",
                                },
                                t.i({ className: "ri-more-line", ariaHidden: true }),
                            ),
                            t.div(
                                {
                                    id: uniqueId + "modal-header-dropdown",
                                    className: "dropdown nowrap modal-header-dropdown",
                                    popover: "auto",
                                },
                                // auth only options
                                () => {
                                    if (!data.isAuthCollection) {
                                        return;
                                    }

                                    const options = [];

                                    if (
                                        !data.originalRecord.verified
                                        && data.originalRecord.email
                                        // superusers are always verified
                                        && !data.isSuperusersCollection
                                    ) {
                                        options.push(sendVerificationDropdownItem(collection, data, modalSettings));
                                    }

                                    if (data.originalRecord.email) {
                                        options.push(
                                            sendPasswordResetEmailDropdownItem(collection, data, modalSettings),
                                        );
                                    }

                                    options.push(impersonateDropdownItem(collection, data, modalSettings));
                                    options.push(resetTokenKeyDropdownItem(collection, data, modalSettings));
                                    options.push(t.hr());

                                    return options;
                                },
                                t.button(
                                    {
                                        type: "button",
                                        className: "dropdown-item",
                                        onclick: (e) => {
                                            e.target.closest(".dropdown").hidePopover();
                                            app.utils.copyToClipboard(
                                                JSON.stringify(data.originalRecord, redactedReplacer, 2),
                                            );
                                            app.toasts.success("Record copied to clipboard!");
                                        },
                                    },
                                    t.i({ className: "ri-braces-line", ariaHidden: true }),
                                    t.span({ className: "txt" }, "Copy JSON"),
                                ),
                                () => {
                                    if (collection.type == "view") {
                                        return;
                                    }

                                    return [
                                        t.button(
                                            {
                                                type: "button",
                                                className: "dropdown-item",
                                                onclick: (e) => {
                                                    e.target.closest(".dropdown").hidePopover();

                                                    if (data.hasChanges) {
                                                        app.modals.confirm(
                                                            "You have unsaved changes. Do you really want to discard them?",
                                                            duplicate,
                                                            null,
                                                            { yesButton: "Yes, discard" },
                                                        );
                                                    } else {
                                                        duplicate();
                                                    }
                                                },
                                            },
                                            t.i({ className: "ri-file-copy-line", ariaHidden: true }),
                                            t.span({ className: "txt" }, "Duplicate"),
                                        ),
                                        t.hr(),
                                        deleteDropdownItem(collection, data, modalSettings),
                                    ];
                                },
                            ),
                        ];
                    },
                ),
                () => {
                    if (!data.showTabs) {
                        return;
                    }

                    return t.div(
                        { className: "col-12" },
                        t.nav(
                            { className: "tabs-header equal-width" },
                            t.button(
                                {
                                    type: "button",
                                    disabled: () => data.isLoading || data.isSaving,
                                    className: () =>
                                        `tab-item ${
                                            data.activeTab == TAB_MAIN ? "active" : data.hasChanges ? "txt-warning" : ""
                                        }`,
                                    ariaDescription: app.attrs.tooltip(() =>
                                        data.hasChanges && data.activeTab != TAB_MAIN ? "Has unsaved changes" : ""
                                    ),
                                    onclick: () => data.activeTab = TAB_MAIN,
                                },
                                t.span({ className: "txt" }, () => (data.isAuthCollection ? "Account" : "Main")),
                            ),
                            t.button(
                                {
                                    type: "button",
                                    disabled: () => data.isLoading || data.isSaving,
                                    className: () => `tab-item ${data.activeTab == TAB_AUTH_PROVIDERS ? "active" : ""}`,
                                    onclick: () => data.activeTab = TAB_AUTH_PROVIDERS,
                                },
                                t.span({ className: "txt" }, "Auth providers"),
                            ),
                        ),
                    );
                },
            ),
        ),
        () => {
            if (
                !data.isNew
                && !data.isSuperusersCollection
                && data.activeTab == TAB_AUTH_PROVIDERS
            ) {
                return authProvidersTab(collection, data);
            }

            return mainTab();
        },
    );

    return modal;
}

// dropdown options
// -------------------------------------------------------------------

function resetTokenKeyDropdownItem(collection, data, modalSettings) {
    const local = store({
        isSubmitting: false,
    });

    async function resetTokenKey() {
        if (local.isSubmitting || !data.record.id) {
            return;
        }

        local.isSubmitting = true;

        try {
            const payload = {};

            const tokenKeyField = collection.fields.find((f) => f.name == "tokenKey");
            if (tokenKeyField.autogeneratePattern) {
                // leave the server to create the random string
                payload["tokenKey:autogenerate"] = "";
            } else {
                // set it manually
                payload["tokenKey"] = app.utils.randomSecret(
                    tokenKeyField.max << 0 || Math.max(2 * tokenKeyField.min << 0, 50),
                );
            }

            const updatedRecord = await app.pb.collection(collection.name).update(data.record.id, payload);

            modalSettings.ontokensreset?.(updatedRecord);

            // refresh all autodate fields
            const fields = collection.fields?.filter((f) => f.type == "autodate") || [];
            for (const field of fields) {
                const val = updatedRecord[field.name];
                if (data.initialDraft) {
                    data.initialDraft[field.name] = val;
                }
                data.originalRecord[field.name] = val;
                data.record[field.name] = val;
            }

            app.toasts.success("Successfully reset all tokens for the selected record.");
        } catch (err) {
            app.checkApiError(err);
        }

        local.isSubmitting = false;
    }

    return t.button(
        {
            type: "button",
            className: "dropdown-item",
            disabled: () => local.isSubmitting,
            onclick: (e) => {
                e.target.closest(".dropdown").hidePopover();
                app.modals.confirm(
                    "Do you really want to reset all issued tokens for the selected auth record?",
                    resetTokenKey,
                    null,
                    { yesButton: "Reset all tokens" },
                );
            },
        },
        t.i({ className: "ri-reset-left-line", ariaHidden: true }),
        t.span({ className: "txt" }, "Reset issued tokens"),
    );
}

function sendPasswordResetEmailDropdownItem(collection, data, modalSettings) {
    const local = store({
        isSubmitting: false,
    });

    async function sendPasswordResetEmail() {
        if (local.isSubmitting || !data.originalRecord?.email) {
            return;
        }

        local.isSubmitting = true;

        try {
            await app.pb.collection(collection.name).requestPasswordReset(data.originalRecord.email);

            modalSettings.onpasswordresetsend?.(JSON.parse(JSON.stringify(data.originalRecord)));

            app.toasts.success(`Successfully sent password reset email to ${data.originalRecord.email}.`);
        } catch (err) {
            app.checkApiError(err);
        }

        local.isSubmitting = false;
    }

    return t.button(
        {
            type: "button",
            className: "dropdown-item",
            disabled: () => local.isSubmitting,
            onclick: (e) => {
                e.target.closest(".dropdown").hidePopover();
                app.modals.confirm(
                    `Do you really want to send password reset email to ${data.originalRecord?.email}?`,
                    sendPasswordResetEmail,
                    null,
                    { yesButton: "Send" },
                );
            },
        },
        t.i({ className: "ri-mail-lock-line", ariaHidden: true }),
        t.span({ className: "txt" }, "Send password reset email"),
    );
}

function sendVerificationDropdownItem(collection, data, modalSettings) {
    const local = store({
        isSubmitting: false,
    });

    async function sendVerificationEmail() {
        if (local.isSubmitting || !data.originalRecord?.email || data.originalRecord?.verified) {
            return;
        }

        local.isSubmitting = true;

        try {
            await app.pb.collection(collection.name).requestVerification(data.originalRecord.email);

            modalSettings.onverificationsend?.(JSON.parse(JSON.stringify(data.originalRecord)));

            app.toasts.success(`Successfully sent verification email to ${data.originalRecord.email}.`);
        } catch (err) {
            app.checkApiError(err);
        }

        local.isSubmitting = false;
    }

    return t.button(
        {
            type: "button",
            className: "dropdown-item",
            disabled: () => local.isSubmitting,
            onclick: (e) => {
                e.target.closest(".dropdown").hidePopover();
                app.modals.confirm(
                    `Do you really want to send verification email to ${data.originalRecord?.email}?`,
                    sendVerificationEmail,
                    null,
                    { yesButton: "Send" },
                );
            },
        },
        t.i({ className: "ri-mail-check-line", ariaHidden: true }),
        t.span({ className: "txt" }, "Send verification email"),
    );
}

function impersonateDropdownItem(collection, data, modalSettings) {
    return t.button(
        {
            type: "button",
            className: "dropdown-item",
            onclick: (e) => {
                e.target.closest(".dropdown").hidePopover();
                app.modals.openRecordImpersontate(data.originalRecord);
            },
        },
        t.i({ className: "ri-id-card-line", ariaHidden: true }),
        t.span({ className: "txt" }, "Impersonate"),
    );
}

function deleteDropdownItem(collection, data, modalSettings) {
    const local = store({
        isSubmitting: false,
    });

    async function deleteRecord() {
        if (local.isSubmitting || !data.originalRecord?.id) {
            return;
        }

        local.isSubmitting = true;

        try {
            await app.pb.collection(collection.name).delete(data.originalRecord.id);

            modalSettings.ondelete?.(JSON.parse(JSON.stringify(data.originalRecord)));

            app.toasts.success(`Successfully deleted record "${data.originalRecord.id}".`);
        } catch (err) {
            app.checkApiError(err);
        }

        local.isSubmitting = false;
    }

    return t.button(
        {
            type: "button",
            className: "dropdown-item txt-danger",
            disabled: () => local.isSubmitting,
            onclick: (e) => {
                e.target.closest(".dropdown").hidePopover();
                app.modals.confirm(
                    `Do you really want to delete the selected record?`,
                    async () => {
                        await deleteRecord();
                        app.modals.close(e.target.closest(".modal"));
                    },
                    null,
                    { yesButton: "Delete record" },
                );
            },
        },
        t.i({ className: "ri-delete-bin-7-line", ariaHidden: true }),
        t.span({ className: "txt" }, "Delete"),
    );
}

// auth specific fields
// -------------------------------------------------------------------

function authFieldEmail(collection, data) {
    const emailField = collection.fields.find((f) => f.name == "email");
    if (!emailField) {
        console.warn("missing expected email field");
        return;
    }

    const uniqueId = "auth_email_" + app.utils.randomString();

    return t.div(
        { className: "record-field-input field-type-email field-type-auth-email" },
        t.div(
            { className: "fields" },
            t.div(
                { className: "field" },
                t.label(
                    { htmlFor: uniqueId },
                    t.i({ className: app.fieldTypes.email.icon, ariaHidden: true }),
                    t.span({ className: "txt" }, () => emailField.name),
                ),
                t.input({
                    type: "email",
                    id: uniqueId,
                    spellcheck: false,
                    name: () => emailField.name,
                    required: () => emailField.required,
                    value: () => data.record[emailField.name] || "",
                    oninput: (e) => (data.record[emailField.name] = e.target.value),
                }),
            ),
            t.div(
                { className: "field addon" },
                t.button(
                    {
                        type: "button",
                        className: () => `btn sm transparent ${data.record.emailVisibility ? "success" : "secondary"}`,
                        ariaDescription: app.attrs.tooltip("Make email public or private", "top-right"),
                        onclick: () => {
                            data.record.emailVisibility = !data.record.emailVisibility;
                        },
                    },
                    t.span({ className: "txt" }, "Public: ", () => (data.record.emailVisibility ? "On" : "Off")),
                ),
            ),
        ),
        () => {
            if (emailField.help) {
                return t.div({ className: "field-help" }, emailField.help);
            }
        },
    );
}

function authFieldVerified(collection, data) {
    const verifiedField = collection.fields.find((f) => f.name == "verified");
    if (!verifiedField) {
        console.warn("missing expected verified field");
        return;
    }

    const elem = app.fieldTypes.bool.input({
        get field() {
            return verifiedField;
        },
        get collection() {
            return collection;
        },
        get record() {
            return data.record;
        },
        get originalRecord() {
            return data.originalRecord;
        },
    });

    elem.addEventListener("change", (e) => {
        if (data.originalRecord.verified == data.record.verified) {
            return;
        }

        app.modals.confirm(
            `Do you really want to manually change the verified account state from "${!data.record
                .verified}" to "${data.record.verified}"?`,
            null,
            () => {
                data.record.verified = !data.record.verified;
            },
            { yesButton: "Yes, " + (data.record.verified ? "verify" : "unverify") },
        );
    });

    return elem;
}

function authFieldPassword(collection, data) {
    const uniqueId = "auth_pass_" + app.utils.randomString();

    const local = store({
        changePassword: false,
        get isNew() {
            return app.utils.isEmpty(data.originalRecord?.id);
        },
    });

    function clearPasswords() {
        delete data.record.password;
        delete data.record.passwordConfirm;

        if (app.store.errors) {
            delete app.store.errors.password;
            delete app.store.errors.passwordConfirm;
        }
    }

    return t.div(
        {
            className: "record-field-input field-type-password field-type-auth-password",
            onmount: (el) => {
                el._watchers?.forEach((w) => w?.unwatch());
                el._watchers = [
                    // force the toggle if any of the fields are populated
                    // (e.g. record update from outside) or there is an error
                    watch(() => {
                        if (local.changePassword) {
                            return; // already enabled
                        }

                        if (
                            app.store.errors?.password
                            || app.store.errors?.passwordConfirm
                            || data.record.password
                            || data.record.passwordConfirm
                        ) {
                            local.changePassword = true;
                        }
                    }),
                ];
            },
            onunmount: (el) => {
                el._watchers?.forEach((w) => w?.unwatch());
            },
        },
        t.div(
            {
                hidden: () => local.isNew,
                className: "field",
            },
            t.input({
                type: "checkbox",
                id: uniqueId + "_change",
                className: "switch",
                checked: () => local.changePassword,
                onchange: (e) => {
                    local.changePassword = e.target.checked;
                    if (!e.target.checked) {
                        clearPasswords();
                    }
                },
            }),
            t.label({ htmlFor: uniqueId + "_change" }, t.span({ className: "txt" }, "change password")),
        ),
        app.components.slide(
            () => local.isNew || local.changePassword,
            t.div(
                { className: () => `fields ${local.isNew ? "" : "m-t-sm"}` },
                t.div(
                    { className: "field" },
                    t.label(
                        { htmlFor: uniqueId + "_password" },
                        t.i({ className: "ri-lock-line", ariaHidden: true }),
                        t.span({ className: "txt" }, "Password"),
                    ),
                    t.input({
                        type: "password",
                        id: uniqueId + "_password",
                        spellcheck: false,
                        name: "password",
                        className: "inline-error",
                        autocomplete: "new-password",
                        required: () => local.isNew || local.changePassword,
                        value: () => data.record.password || "",
                        oninput: (e) => {
                            if (!e.target.value) {
                                // delete to ensure that it is not submitted
                                delete data.record.password;
                            } else {
                                data.record.password = e.target.value;
                            }
                        },
                    }),
                ),
                t.div({ className: "delimiter" }),
                t.div(
                    { className: "field" },
                    t.label(
                        { htmlFor: uniqueId + "_password_confirm" },
                        t.i({ className: "ri-lock-line", ariaHidden: true }),
                        t.span({ className: "txt" }, "Confirm"),
                    ),
                    t.input({
                        type: "password",
                        id: uniqueId + "_password_confirm",
                        spellcheck: false,
                        name: "passwordConfirm",
                        className: "inline-error",
                        autocomplete: "new-password",
                        required: () => local.isNew || local.changePassword,
                        value: () => data.record.passwordConfirm || "",
                        oninput: (e) => {
                            if (!e.target.value) {
                                // delete to ensure that it is not submitted
                                delete data.record.passwordConfirm;
                            } else {
                                data.record.passwordConfirm = e.target.value;
                            }
                        },
                    }),
                ),
            ),
            () => {
                const helpText = collection.fields?.find((f) => f.name == "password")?.help || "";
                if (!helpText) {
                    return;
                }

                return t.div({ className: "field-help" }, helpText);
            },
            t.div(
                { className: "field-help" },
                t.span(
                    {
                        className: "txt link-hint",
                        role: "button",
                        onclick: (e) => {
                            e.preventDefault();
                            const random = app.utils.randomSecret(20);
                            data.record.password = random;
                            data.record.passwordConfirm = random;
                            app.utils.copyToClipboard(random);
                            app.toasts.info("Generated and copied random password to clipboard.");
                        },
                    },
                    "Generate and set random password",
                ),
            ),
        ),
    );
}

function authProvidersTab(collection, data) {
    const local = store({
        isLoading: false,
        externalAuths: [],
    });

    async function loadExternalAuths() {
        local.isLoading = true;

        try {
            local.externalAuths = await app.pb.collection("_externalAuths").getFullList({
                filter: app.pb.filter("collectionRef={:collectionId} && recordRef={:recordId}", {
                    collectionId: data.record.collectionId,
                    recordId: data.record.id,
                }),
            });

            local.isLoading = false;
        } catch (err) {
            if (err?.isAbort) {
                app.pb.checkApiError(err);
                local.isLoading = false;
            }
        }
    }

    function confirmAndUnlink(externalAuth) {
        const providerInfo = app.store.oauth2Providers?.find((p) => p.name == externalAuth.provider) || {};
        const name = providerInfo.displayName || externalAuth.provider;

        app.modals.confirm(
            `Do you really want to unlink the ${name} provider?`,
            () => {
                return app.pb
                    .collection("_externalAuths")
                    .delete(externalAuth.id)
                    .then(() => {
                        app.toasts.success(`Successfully unlinked ${name}.`);
                        loadExternalAuths(); // reload list
                    })
                    .catch((err) => {
                        app.checkApiError(err);
                    });
            },
            null,
            { yesButton: "Unlink" },
        );
    }

    return [
        t.div(
            { className: "modal-content" },
            t.div(
                {
                    className: "list",
                    onmount: () => {
                        loadExternalAuths();
                    },
                },
                () => {
                    if (local.isLoading) {
                        return t.div({ className: "list-item" }, t.div({ className: "skeleton-loader" }));
                    }

                    if (!local.externalAuths.length) {
                        return t.div(
                            { className: "list-item" },
                            t.div({ className: "block txt-hint txt-center" }, "No external auth providers found."),
                        );
                    }

                    return local.externalAuths.map((externalAuth) => {
                        const providerInfo = app.store.oauth2Providers?.find((p) => p.name == externalAuth.provider)
                            || {};

                        return t.div(
                            { className: "list-item" },
                            t.figure(
                                { className: "provider-logo" },
                                () => {
                                    if (providerInfo.logo) {
                                        return t.img({
                                            src: "data:image/svg+xml;base64," + btoa(providerInfo.logo),
                                            alt: externalAuth.provider + " logo",
                                        });
                                    }

                                    return t.i({ className: app.utils.fallbackProviderIcon, ariaHidden: true });
                                },
                            ),
                            t.div(
                                { className: "content" },
                                t.span(
                                    { className: "txt-nowrap" },
                                    () => providerInfo.displayName || externalAuth.provider,
                                ),
                                t.small({ className: "txt-hint" }, "ID: ", () => externalAuth.providerId),
                            ),
                            t.div(
                                { className: "actions" },
                                t.button(
                                    {
                                        type: "button",
                                        className: "btn sm secondary transparent circle",
                                        ariaLabel: app.attrs.tooltip("Unlink", "left"),
                                        onclick: () => confirmAndUnlink(externalAuth),
                                    },
                                    t.i({ className: "ri-close-line", ariaHidden: true }),
                                ),
                            ),
                        );
                    });
                },
            ),
        ),
        t.footer(
            { className: "modal-footer" },
            t.button(
                {
                    type: "button",
                    className: "btn transparent m-r-auto",
                    onclick: () => app.modals.close(),
                },
                t.span({ className: "txt" }, "Close"),
            ),
        ),
    ];
}
