import {
    buildAvatarList,
    characterToEntity,
    characters,
    chat,
    chat_metadata,
    createOrEditCharacter,
    default_user_avatar,
    eventSource,
    event_types,
    getCurrentChatId,
    getRequestHeaders,
    getThumbnailUrl,
    groupToEntity,
    menu_type,
    name1,
    name2,
    reloadCurrentChat,
    saveChatConditional,
    saveMetadata,
    saveSettingsDebounced,
    setUserName,
    this_chid,
} from '../script.js';
import { power_user } from './power-user.js';
import { getTokenCountAsync } from './tokenizers.js';
import {
    PAGINATION_TEMPLATE,
    clearInfoBlock,
    debounce,
    delay,
    download,
    ensureImageFormatSupported,
    flashHighlight,
    getBase64Async,
    getCharIndex,
    isFalseBoolean,
    isTrueBoolean,
    onlyUnique,
    parseJsonFile,
    setInfoBlock,
    localizePagination,
    renderPaginationDropdown,
    paginationDropdownChangeHandler,
    addLongPressEvent,
    stringToRange,
    sortIgnoreCaseAndAccents,
    equalsIgnoreCaseAndAccents,
    uuidv4,
    resolveAvatarData,
    findPersona,
    escapeHtml,
} from './utils.js';
import { debounce_timeout } from './constants.js';
import { FILTER_TYPES, FilterHelper } from './filters.js';
import { groups, selected_group } from './group-chats.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { t } from './i18n.js';
import { openWorldInfoEditor, world_names } from './world-info.js';
import { renderTemplateAsync } from './templates.js';
import { saveMetadataDebounced } from './extensions.js';
import { accountStorage } from './util/AccountStorage.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { SlashCommandNamedArgument, ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
import { commonEnumMatchProviders, commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { isFirefox } from './browser-fixes.js';
import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';

/**
 * @typedef {object} PersonaConnection A connection between a character and a character or group entity
 * @property {'character' | 'group'} type - Type of connection
 * @property {string} id - ID of the connection (character key (avatar url), group id)
 */

/** @typedef {'chat' | 'character' | 'default'} PersonaLockType Type of the persona lock */

/**
 * @typedef {object} PersonaState
 * @property {string} avatarId - The avatar id of the persona
 * @property {boolean} default - Whether this persona is the default one for all new chats
 * @property {object} locked - An object containing the lock states
 * @property {boolean} locked.chat - Whether the persona is locked to the currently open chat
 * @property {boolean} locked.character - Whether the persona is locked to the currently open character or group
 */

export const persona_description_positions = {
    IN_PROMPT: 0,
    /**
     * @deprecated Use persona_description_positions.IN_PROMPT instead.
     */
    AFTER_CHAR: 1,
    TOP_AN: 2,
    BOTTOM_AN: 3,
    AT_DEPTH: 4,
    NONE: 9,
};

const USER_AVATAR_PATH = 'User Avatars/';

let savePersonasPage = 0;
const GRID_STORAGE_KEY = 'Personas_GridView';
const DEFAULT_DEPTH = 2;
const DEFAULT_ROLE = 0;

/** @type {string} The currently selected persona (identified by its avatar) */
export let user_avatar = '';

/** @type {FilterHelper} Filter helper for the persona list */
export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick));

/** @type {string} The last loaded chat id to remember for persona loading */
let personaLastLoadedChatId = null;

/** @type {function(string): void} */
let navigateToAvatar = () => { };

/**
 * Checks if the Persona Management panel is currently open
 * @returns {boolean}
 */
export function isPersonaPanelOpen() {
    return document.querySelector('#persona-management-button .drawer-content')?.classList.contains('openDrawer') ?? false;
}

function switchPersonaGridView() {
    const state = accountStorage.getItem(GRID_STORAGE_KEY) === 'true';
    $('#user_avatar_block').toggleClass('gridView', state);
}

/**
 * Returns the URL of the avatar for the given user avatar Id.
 * @param {string} avatarImg User avatar Id
 * @returns {string} User avatar URL
 */
export function getUserAvatar(avatarImg) {
    return `${USER_AVATAR_PATH}${avatarImg}`;
}

export function initUserAvatar(avatar) {
    user_avatar = avatar;
    reloadUserAvatar();
    updatePersonaUIStates();
}

/**
 * Sets a user avatar file
 * @param {string} imgfile Link to an image file
 * @param {object} [options] Optional settings
 * @param {boolean} [options.toastPersonaNameChange=true] Whether to show a toast when the persona name is changed
 * @param {boolean} [options.navigateToCurrent=false] Whether to navigate to the current persona after setting the avatar
 */
export async function setUserAvatar(imgfile, { toastPersonaNameChange = true, navigateToCurrent = false } = {}) {
    const currentUserAvatar = user_avatar;
    user_avatar = imgfile && typeof imgfile === 'string' ? imgfile : $(this).attr('data-avatar-id');
    if (currentUserAvatar === user_avatar) {
        return;
    }
    reloadUserAvatar();
    updatePersonaUIStates({ navigateToCurrent: navigateToCurrent });
    selectCurrentPersona({ toastPersonaNameChange: toastPersonaNameChange });
    await retriggerFirstMessageOnEmptyChat();
    saveSettingsDebounced();
    $('.zoomed_avatar[forchar]').remove();
    await eventSource.emit(event_types.PERSONA_CHANGED, user_avatar);
}

function reloadUserAvatar(force = false) {
    $('.mes').each(function () {
        const avatarImg = $(this).find('.avatar img');
        if (force) {
            avatarImg.attr('src', avatarImg.attr('src'));
        }

        if ($(this).attr('is_user') == 'true' && $(this).attr('force_avatar') == 'false') {
            avatarImg.attr('src', getThumbnailUrl('persona', user_avatar));
        }
    });
}

/**
 * Sort the given personas
 * @param {string[]} personas - The persona names to sort
 * @returns {string[]} The sorted persona names array, same reference as passed in
 */
function sortPersonas(personas) {
    const option = $('#persona_sort_order').find(':selected');
    if (option.attr('value') === 'search') {
        personas.sort((a, b) => {
            const aScore = personasFilter.getScore(FILTER_TYPES.PERSONA_SEARCH, a);
            const bScore = personasFilter.getScore(FILTER_TYPES.PERSONA_SEARCH, b);
            return (aScore - bScore);
        });
    } else {
        personas.sort((a, b) => {
            const aName = String(power_user.personas[a] || a);
            const bName = String(power_user.personas[b] || b);
            return power_user.persona_sort_order === 'asc' ? aName.localeCompare(bName) : bName.localeCompare(aName);
        });
    }

    return personas;
}

/** Checks the state of the current search, and adds/removes the search sorting option accordingly */
function verifyPersonaSearchSortRule() {
    const searchTerm = personasFilter.getFilterData(FILTER_TYPES.PERSONA_SEARCH);
    const searchOption = $('#persona_sort_order option[value="search"]');
    const selector = $('#persona_sort_order');
    const isHidden = searchOption.attr('hidden') !== undefined;

    // If we have a search term, we are displaying the sorting option for it
    if (searchTerm && isHidden) {
        searchOption.removeAttr('hidden');
        selector.val(searchOption.attr('value'));
        flashHighlight(selector);
    }
    // If search got cleared, we make sure to hide the option and go back to the one before
    if (!searchTerm) {
        searchOption.attr('hidden', '');
        selector.val(power_user.persona_sort_order);
    }
}

/**
 * Gets a rendered avatar block.
 * @param {string} avatarId Avatar file name
 * @returns {JQuery<HTMLElement>} Avatar block
 */
function getUserAvatarBlock(avatarId) {
    const template = $('#user_avatar_template .avatar-container').clone();
    const personaName = power_user.personas[avatarId];
    const personaDescription = power_user.persona_descriptions[avatarId]?.description;
    const personaTitle = power_user.persona_descriptions[avatarId]?.title;

    template.find('.ch_name').text(personaName || '[Unnamed Persona]');
    template.find('.ch_description').text(personaDescription || $('#user_avatar_block').attr('no_desc_text')).toggleClass('text_muted', !personaDescription);
    template.find('.ch_additional_info').text(personaTitle || '');
    template.attr('data-avatar-id', avatarId);
    template.find('.avatar').attr('data-avatar-id', avatarId).attr('title', avatarId);
    template.toggleClass('default_persona', avatarId === power_user.default_persona);
    const avatarUrl = getThumbnailUrl('persona', avatarId, isFirefox());
    template.find('img').attr('src', avatarUrl);

    // Make sure description block has at least three rows. Otherwise height looks inconsistent. I don't have a better idea for this.
    const currentText = template.find('.ch_description').text();
    if (currentText.split('\n').length < 3) {
        template.find('.ch_description').text(currentText + '\n\xa0\n\xa0');
    }

    $('#user_avatar_block').append(template);
    return template;
}

/**
 * Initialize missing personas in the power user settings.
 * @param {string[]} avatarsList List of avatar file names
 * @returns {Promise<void>}
 */
async function addMissingPersonas(avatarsList) {
    for (const persona of avatarsList) {
        if (!power_user.personas[persona]) {
            await initPersona(persona, '[Unnamed Persona]', '', '', { silent: true });
        }
    }
}

/**
 * Gets a list of user avatars.
 * @param {boolean} doRender Whether to render the list
 * @param {string} openPageAt Item to be opened at
 * @returns {Promise<string[]>} List of avatar file names
 */
export async function getUserAvatars(doRender = true, openPageAt = '') {
    const response = await fetch('/api/avatars/get', {
        method: 'POST',
        headers: getRequestHeaders({ omitContentType: true }),
    });
    if (response.ok) {
        const allEntities = await response.json();

        if (!Array.isArray(allEntities)) {
            return [];
        }

        if (!doRender) {
            return allEntities;
        }

        // If any persona is missing from the power user settings, we add it
        await addMissingPersonas(allEntities);
        // Before printing the personas, we check if we should enable/disable search sorting
        verifyPersonaSearchSortRule();

        let entities = personasFilter.applyFilters(allEntities);
        entities = sortPersonas(entities);

        const storageKey = 'Personas_PerPage';
        const listId = '#user_avatar_block';
        const perPage = Number(accountStorage.getItem(storageKey)) || 5;
        const sizeChangerOptions = [5, 10, 25, 50, 100, 250, 500, 1000];

        $('#persona_pagination_container').pagination({
            dataSource: entities,
            pageSize: perPage,
            sizeChangerOptions,
            pageRange: 1,
            pageNumber: savePersonasPage || 1,
            position: 'top',
            showPageNumbers: false,
            showSizeChanger: true,
            formatSizeChanger: renderPaginationDropdown(perPage, sizeChangerOptions),
            prevText: '<',
            nextText: '>',
            formatNavigator: PAGINATION_TEMPLATE,
            showNavigator: true,
            callback: function (data) {
                $(listId).empty();
                for (const item of data) {
                    $(listId).append(getUserAvatarBlock(item));
                }
                updatePersonaUIStates();
                localizePagination($('#persona_pagination_container'));
            },
            afterSizeSelectorChange: function (e, size) {
                accountStorage.setItem(storageKey, e.target.value);
                paginationDropdownChangeHandler(e, size);
            },
            afterPaging: function (e) {
                savePersonasPage = e;
            },
            afterRender: function () {
                $(listId).scrollTop(0);
            },
        });

        navigateToAvatar = (avatarId) => {
            const avatarIndex = entities.indexOf(avatarId);
            const page = Math.floor(avatarIndex / perPage) + 1;

            if (avatarIndex !== -1) {
                $('#persona_pagination_container').pagination('go', page);
            }
        };

        openPageAt && navigateToAvatar(openPageAt);

        return allEntities;
    }
}

/**
 * Uploads an avatar file to the server
 * @param {string} url URL for the avatar file
 * @param {string} [name] Optional name for the avatar file
 * @returns {Promise} Promise that resolves when the avatar is uploaded
 */
async function uploadUserAvatar(url, name) {
    const fetchResult = await fetch(url);
    const blob = await fetchResult.blob();
    const file = new File([blob], 'avatar.png', { type: 'image/png' });
    const formData = new FormData();
    formData.append('avatar', file);

    if (name) {
        formData.append('overwrite_name', name);
    }

    const response = await fetch('/api/avatars/upload', {
        method: 'POST',
        headers: getRequestHeaders({ omitContentType: true }),
        cache: 'no-cache',
        body: formData,
    });

    if (!response.ok) {
        throw new Error(`Failed to upload avatar: ${response.statusText}`);
    }

    // Get the actual path from the response
    const data = await response.json();
    await getUserAvatars(true, data?.path || name);
}

async function changeUserAvatar(e) {
    const form = document.getElementById('form_upload_avatar');

    if (!(form instanceof HTMLFormElement)) {
        console.error('Form not found');
        return;
    }

    const file = e.target.files[0];

    if (!file) {
        form.reset();
        return;
    }

    const formData = new FormData(form);
    const dataUrl = await getBase64Async(file);
    let url = '/api/avatars/upload';

    if (!power_user.never_resize_avatars) {
        const dlg = new Popup(t`Set the crop position of the avatar image`, POPUP_TYPE.CROP, '', { cropImage: dataUrl });
        const result = await dlg.show();

        if (!result) {
            return;
        }

        if (dlg.cropData !== undefined) {
            url += `?crop=${encodeURIComponent(JSON.stringify(dlg.cropData))}`;
        }
    }

    const rawFile = formData.get('avatar');
    if (rawFile instanceof File) {
        const convertedFile = await ensureImageFormatSupported(rawFile);
        formData.set('avatar', convertedFile);
    }

    const response = await fetch(url, {
        method: 'POST',
        headers: getRequestHeaders({ omitContentType: true }),
        cache: 'no-cache',
        body: formData,
    });

    if (response.ok) {
        const data = await response.json();

        const overwriteName = formData.get('overwrite_name');
        const dataPath = data?.path;

        // If the user uploaded a new avatar, we want to make sure it's not cached
        if (overwriteName && dataPath) {
            await fetch(getUserAvatar(String(dataPath)), { cache: 'reload' });
            await fetch(getThumbnailUrl('persona', String(dataPath)), { cache: 'reload' });
            reloadUserAvatar(true);
        }

        if (!overwriteName && dataPath) {
            await getUserAvatars();
            await delay(1);
            await createPersona(dataPath);
        }

        await getUserAvatars(true, dataPath || overwriteName);
    }

    // Will allow to select the same file twice in a row
    form.reset();
}

/**
 * Prompts the user to create a persona for the uploaded avatar.
 * @param {string} avatarId User avatar id
 * @returns {Promise} Promise that resolves when the persona is set
 */
export async function createPersona(avatarId) {
    const personaName = await Popup.show.input(t`Enter a name for this persona:`, t`Cancel if you're just uploading an avatar.`, '');

    if (!personaName) {
        console.debug('User cancelled creating a persona');
        return;
    }

    const personaDescription = await Popup.show.input(t`Enter a description for this persona:`, t`You can always add or change it later.`, '', { rows: 4 });

    await initPersona(avatarId, personaName, personaDescription, '');
    if (power_user.persona_show_notifications) {
        toastr.success(t`You can now pick ${personaName} as a persona in the Persona Management menu.`, t`Persona Created`);
    }
}

async function createDummyPersona() {
    const popup = new Popup(t`Enter a name for this persona:`, POPUP_TYPE.INPUT, '', {
        customInputs: [{
            id: 'persona_title',
            type: 'text',
            label: t`Persona Title (optional, display only)`,
        }],
    });

    const personaName = await popup.show();
    const personaTitle = String(popup.inputResults.get('persona_title') || '').trim();

    if (!personaName || typeof personaName !== 'string') {
        console.debug('User cancelled creating dummy persona');
        return;
    }

    // Date + name (only ASCII) to make it unique
    const avatarId = `${Date.now()}-${personaName.replace(/[^a-zA-Z0-9]/g, '')}.png`;
    await initPersona(avatarId, personaName, '', personaTitle);
    await uploadUserAvatar(default_user_avatar, avatarId);
}

/**
 * Initializes a persona for the given avatar id.
 * @param {string} avatarId User avatar id
 * @param {string} personaName Name for the persona
 * @param {string} personaDescription Optional description for the persona
 * @param {string} personaTitle Optional title for the persona
 * @param {object} [options={}] Optional settings
 * @param {boolean} [options.silent=false] If true, no PERSONA_CREATED event is emitted (used for background migrations)
 * @param {number} [options.position=persona_description_positions.IN_PROMPT] Description position (defaults to IN_PROMPT)
 * @param {number} [options.depth=DEFAULT_DEPTH] Description depth (defaults to DEFAULT_DEPTH)
 * @param {number} [options.role=DEFAULT_ROLE] Description role (defaults to DEFAULT_ROLE)
 * @param {string} [options.lorebook=''] Attached lorebook name
 * @returns {Promise<void>}
 */
export async function initPersona(avatarId, personaName, personaDescription, personaTitle, {
    silent = false,
    position = persona_description_positions.IN_PROMPT,
    depth = DEFAULT_DEPTH,
    role = DEFAULT_ROLE,
    lorebook = '',
} = {}) {
    power_user.personas[avatarId] = personaName;
    power_user.persona_descriptions[avatarId] = {
        description: personaDescription || '',
        position: position,
        depth: depth,
        role: role,
        lorebook: lorebook,
        title: personaTitle || '',
    };

    saveSettingsDebounced();

    if (!silent) {
        await eventSource.emit(event_types.PERSONA_CREATED, { avatarId, name: personaName, description: personaDescription || '', title: personaTitle || '' });
    }
}

/**
 * Converts a character given character (either by character id or the current character) to a persona.
 *
 * If a persona with the same name already exists, the user is prompted to confirm whether or not to overwrite it.
 * If the character description contains {{char}} or {{user}} macros, the user is prompted to confirm whether or not to swap them for persona macros.
 *
 * The function creates a new persona with the same name as the character, and sets the persona description to the character description with the macros swapped.
 * The function also saves the settings and refreshes the persona selector.
 *
 * @param {number} [characterId] - The ID of the character to convert to a persona. Defaults to the current character ID.
 * @returns {Promise<boolean>} A promise that resolves to true if the character was converted, false otherwise.
 */
export async function convertCharacterToPersona(characterId = null) {
    if (null === characterId) characterId = Number(this_chid);

    const avatarUrl = characters[characterId]?.avatar;
    if (!avatarUrl) {
        console.log('No avatar found for this character');
        return false;
    }

    const name = characters[characterId]?.name;
    let description = characters[characterId]?.description;
    const overwriteName = `${name} (Persona).png`;

    if (overwriteName in power_user.personas) {
        const confirm = await Popup.show.confirm(t`Overwrite Existing Persona`, t`This character exists as a persona already. Do you want to overwrite it?`);
        if (!confirm) {
            console.log('User cancelled the overwrite of the persona');
            return false;
        }
    }

    if (description.includes('{{char}}') || description.includes('{{user}}')) {
        const confirm = await Popup.show.confirm(t`Persona Description Macros`, t`This character has a description that uses <code>{{char}}</code> or <code>{{user}}</code> macros. Do you want to swap them in the persona description?`);
        if (confirm) {
            description = description.replace(/{{char}}/gi, '{{personaChar}}').replace(/{{user}}/gi, '{{personaUser}}');
            description = description.replace(/{{personaUser}}/gi, '{{char}}').replace(/{{personaChar}}/gi, '{{user}}');
        }
    }

    const thumbnailAvatar = getThumbnailUrl('avatar', avatarUrl);
    await uploadUserAvatar(thumbnailAvatar, overwriteName);

    power_user.personas[overwriteName] = name;
    power_user.persona_descriptions[overwriteName] = {
        description: description,
        position: persona_description_positions.IN_PROMPT,
        depth: DEFAULT_DEPTH,
        role: DEFAULT_ROLE,
        lorebook: '',
        title: '',
    };

    // If the user is currently using this persona, update the description
    if (user_avatar === overwriteName) {
        power_user.persona_description = description;
    }

    saveSettingsDebounced();
    await eventSource.emit(event_types.PERSONA_CREATED, { avatarId: overwriteName, name, description, title: '' });

    console.log('Persona for character created');
    toastr.success(t`You can now pick ${name} as a persona in the Persona Management menu.`, t`Persona Created`);

    // Refresh the persona selector
    await getUserAvatars(true, overwriteName);
    // Reload the persona description
    setPersonaDescription();
    return true;
}

/**
 * Counts the number of tokens in a persona description.
 */
const countPersonaDescriptionTokens = debounce(async () => {
    const description = String($('#persona_description').val());
    const count = await getTokenCountAsync(description);
    $('#persona_description_token_count').text(String(count));
}, debounce_timeout.relaxed);

/**
 * Updates the UI for the Persona Management page with the current persona values
 */
export function setPersonaDescription() {
    $('#your_name').text(name1);

    if (power_user.persona_description_position === persona_description_positions.AFTER_CHAR) {
        power_user.persona_description_position = persona_description_positions.IN_PROMPT;
    }

    $('#persona_depth_position_settings').toggle(power_user.persona_description_position === persona_description_positions.AT_DEPTH);
    $('#persona_description').val(power_user.persona_description);
    $('#persona_depth_value').val(power_user.persona_description_depth ?? DEFAULT_DEPTH);
    $('#persona_description_position')
        .val(power_user.persona_description_position)
        .find(`option[value="${power_user.persona_description_position}"]`)
        .attr('selected', String(true));
    $('#persona_depth_role')
        .val(power_user.persona_description_role)
        .find(`option[value="${power_user.persona_description_role}"]`)
        .prop('selected', String(true));
    $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook);
    countPersonaDescriptionTokens();

    updatePersonaUIStates();
    updatePersonaConnectionsAvatarList();
}

/**
 * Gets a list of all personas in the current chat.
 *
 * @returns {string[]} An array of persona identifiers
 */
function getPersonasOfCurrentChat() {
    const personas = chat.filter(message => String(message.force_avatar).startsWith(USER_AVATAR_PATH))
        .map(message => message.force_avatar.replace(USER_AVATAR_PATH, ''))
        .filter(onlyUnique);
    return personas;
}

/**
 * Builds a list of persona avatars and populates the given block element with them.
 *
 * @param {HTMLElement} block - The HTML element where the avatar list will be rendered
 * @param {string[]} personas - An array of persona identifiers
 * @param {Object} [options] - Optional settings for building the avatar list
 * @param {boolean} [options.empty=true] - Whether to clear the block element before adding avatars
 * @param {boolean} [options.interactable=false] - Whether the avatars should be interactable
 * @param {boolean} [options.highlightFavs=true] - Whether to highlight favorite avatars
 */
export function buildPersonaAvatarList(block, personas, { empty = true, interactable = false, highlightFavs = true } = {}) {
    const personaEntities = personas.map(avatar => ({
        type: 'persona',
        id: avatar,
        item: {
            name: power_user.personas[avatar],
            description: power_user.persona_descriptions[avatar]?.description || '',
            avatar: avatar,
            fav: power_user.default_persona === avatar,
        },
    }));

    buildAvatarList($(block), personaEntities, { empty: empty, interactable: interactable, highlightFavs: highlightFavs });
}

/**
 * Displays avatar connections for the current persona.
 * Converts connections to entities and populates the avatar list. Shows a message if no connections are found.
 */
export function updatePersonaConnectionsAvatarList() {
    /** @type {PersonaConnection[]} */
    const connections = power_user.persona_descriptions[user_avatar]?.connections ?? [];
    const entities = connections.map(connection => {
        if (connection.type === 'character') {
            const character = characters.find(c => c.avatar === connection.id);
            if (character) return characterToEntity(character, getCharIndex(character));
        }
        if (connection.type === 'group') {
            const group = groups.find(g => g.id === connection.id);
            if (group) return groupToEntity(group);
        }
        return undefined;
    }).filter(entity => entity?.item !== undefined);

    if (entities.length)
        buildAvatarList($('#persona_connections_list'), entities, { interactable: true });
    else
        $('#persona_connections_list').text(t`[No character connections. Click one of the buttons above to connect this persona.]`);
}


/**
 * Displays a popup for persona selection and returns the selected persona.
 *
 * @param {string} title - The title to display in the popup
 * @param {string} text - The text to display in the popup
 * @param {string[]} personas - An array of persona ids to display for selection
 * @param {Object} [options] - Optional settings for the popup
 * @param {string} [options.okButton='None'] - The label for the OK button
 * @param {(element: HTMLElement, ev: MouseEvent) => any} [options.shiftClickHandler] - A function to handle shift-click
 * @param {boolean|string[]} [options.highlightPersonas=false] - Whether to highlight personas - either by providing a list of persona keys, or true to highlight all present in current chat
 * @param {PersonaConnection} [options.targetedChar] - The targeted character or gorup for this persona selection
 * @returns {Promise<string?>} - A promise that resolves to the selected persona id or null if no selection was made
 */
export async function askForPersonaSelection(title, text, personas, { okButton = 'None', shiftClickHandler = undefined, highlightPersonas = false, targetedChar = undefined } = {}) {
    const content = document.createElement('div');
    const titleElement = document.createElement('h3');
    titleElement.textContent = title;
    content.appendChild(titleElement);

    const textElement = document.createElement('div');
    textElement.classList.add('multiline', 'm-b-1');
    textElement.textContent = text;
    content.appendChild(textElement);

    const personaListBlock = document.createElement('div');
    personaListBlock.classList.add('persona-list', 'avatars_inline', 'avatars_multiline', 'text_muted');
    content.appendChild(personaListBlock);

    if (personas.length > 0)
        buildPersonaAvatarList(personaListBlock, personas, { interactable: true });
    else
        personaListBlock.textContent = t`[Currently no personas connected]`;

    const personasToHighlight = highlightPersonas instanceof Array ? highlightPersonas : (highlightPersonas ? getPersonasOfCurrentChat() : []);

    // Make the persona blocks clickable and close the popup
    personaListBlock.querySelectorAll('.avatar[data-type="persona"]').forEach(block => {
        if (!(block instanceof HTMLElement)) return;
        block.dataset.result = String(100 + personas.indexOf(block.dataset.pid));

        if (shiftClickHandler) {
            block.addEventListener('click', function (ev) {
                if (ev.shiftKey) {
                    shiftClickHandler(this, ev);
                }
            });
        }

        if (personasToHighlight && personasToHighlight.includes(block.dataset.pid)) {
            block.classList.add('is_active');
            block.title = block.title + '\n\n' + t`Was used in current chat.`;
            if (block.classList.contains('is_fav')) block.title = block.title + '\n' + t`Is your default persona.`;
        }
    });

    /** @type {import('./popup.js').CustomPopupButton[]} */
    const customButtons = [];
    if (targetedChar) {
        customButtons.push({
            text: t`Remove All Connections`,
            result: 2,
            action: () => {
                for (const [personaId, description] of Object.entries(power_user.persona_descriptions)) {
                    /** @type {PersonaConnection[]} */
                    const connections = description.connections;
                    if (connections) {
                        power_user.persona_descriptions[personaId].connections = connections.filter(c => {
                            if (targetedChar.type == c.type && targetedChar.id == c.id) return false;
                            return true;
                        });
                    }
                }

                saveSettingsDebounced();
                updatePersonaConnectionsAvatarList();
                if (power_user.persona_show_notifications) {
                    const name = targetedChar.type == 'character' ? characters[targetedChar.id]?.name : groups[targetedChar.id]?.name;
                    toastr.info(t`All connections to ${name} have been removed.`, t`Personas Unlocked`);
                }
            },
        });
    }

    const popup = new Popup(content, POPUP_TYPE.TEXT, '', { okButton: okButton, customButtons: customButtons });
    const result = await popup.show();
    return Number(result) >= 100 ? personas[Number(result) - 100] : null;
}

/**
 * Automatically selects a persona based on the given name if a matching persona exists.
 * @param {string} name - The name to search for
 * @param {Object} [options={}]
 * @param {string} [options.personaKey=null] - Optionally a persona avatar key to target (if multiple persona have the same name); must match the name
 * @returns {Promise<boolean>} True if a matching persona was found and selected, false otherwise
 */
export async function autoSelectPersona(name, { personaKey = null } = {}) {
    const persona = findPersona({ name: personaKey ?? name, allowAvatar: !!personaKey });
    if (persona) {
        console.log(`Auto-selecting persona ${persona.avatar} for name ${name}`);
        await setUserAvatar(persona.avatar);
        return true;
    }
    return false;
}

/**
 * Edits the title of a persona based on the input from a popup.
 * @param {Popup} popup Popup instance
 * @param {string} avatarId Avatar ID of the persona to edit
 * @param {string} currentTitle Current title of the persona
 */
async function editPersonaTitle(popup, avatarId, currentTitle) {
    if (popup.result !== POPUP_RESULT.AFFIRMATIVE) {
        return;
    }

    if (!power_user.persona_descriptions[avatarId]) {
        console.warn('Uninitialized persona descriptor for avatar:', avatarId);
        return;
    }

    const newTitle = String(popup.inputResults.get('persona_title') || '').trim();

    if (!newTitle && currentTitle) {
        console.log(`Removed persona title for ${avatarId}`);
        delete power_user.persona_descriptions[avatarId].title;
        await getUserAvatars(true, avatarId);
        saveSettingsDebounced();
        await eventSource.emit(event_types.PERSONA_UPDATED, avatarId);
        return;
    }

    if (newTitle !== currentTitle) {
        power_user.persona_descriptions[avatarId].title = newTitle;
        console.log(`Updated persona title for ${avatarId} to ${newTitle}`);
        await getUserAvatars(true, avatarId);
        saveSettingsDebounced();
        await eventSource.emit(event_types.PERSONA_UPDATED, avatarId);
        return;
    }
}

/**
 * Renames the persona with the given avatar ID by showing a popup to enter a new name.
 * @param {string} avatarId - ID of the avatar to rename
 * @returns {Promise<boolean>} A promise that resolves to true if the persona was renamed, false otherwise
 */
async function renamePersona(avatarId) {
    const currentName = power_user.personas[avatarId];
    const currentTitle = power_user.persona_descriptions[avatarId]?.title || '';
    const newName = await Popup.show.input(t`Rename Persona`, t`Enter a new name for this persona:`, currentName, {
        customInputs: [{
            id: 'persona_title',
            type: 'text',
            label: t`Persona Title (optional, display only)`,
            defaultState: currentTitle,
        }],
        onClose: (popup) => editPersonaTitle(popup, avatarId, currentTitle),
    });

    if (!newName || newName === currentName) {
        console.debug('User cancelled renaming persona or name is unchanged');
        return false;
    }

    power_user.personas[avatarId] = newName;
    console.log(`Renamed persona ${avatarId} to ${newName}`);

    if (avatarId === user_avatar) {
        setUserName(newName);
    }

    saveSettingsDebounced();
    await eventSource.emit(event_types.PERSONA_RENAMED, { avatarId, oldName: currentName, newName });
    await getUserAvatars(true, avatarId);
    updatePersonaUIStates();
    setPersonaDescription();
    return true;
}

/**
 * Selects the persona with the currently set avatar ID by updating the user name and persona description, and updating the locked persona if the setting is enabled.
 * @param {object} [options={}] - Optional settings
 * @param {boolean} [options.toastPersonaNameChange=true] - Whether to show a toast when the persona name is changed
 * @returns {Promise<void>}
 */
async function selectCurrentPersona({ toastPersonaNameChange = true } = {}) {
    const personaName = power_user.personas[user_avatar];
    if (personaName) {
        const shouldAutoLock = power_user.persona_auto_lock && user_avatar !== chat_metadata.persona;

        if (personaName !== name1) {
            console.log(`Auto-updating user name to ${personaName}`);
            setUserName(personaName, { toastPersonaNameChange: !shouldAutoLock && toastPersonaNameChange });
        }

        const descriptor = power_user.persona_descriptions[user_avatar];

        if (descriptor) {
            power_user.persona_description = descriptor.description ?? '';
            power_user.persona_description_position = descriptor.position ?? persona_description_positions.IN_PROMPT;
            power_user.persona_description_depth = descriptor.depth ?? DEFAULT_DEPTH;
            power_user.persona_description_role = descriptor.role ?? DEFAULT_ROLE;
            power_user.persona_description_lorebook = descriptor.lorebook ?? '';
        } else {
            power_user.persona_description = '';
            power_user.persona_description_position = persona_description_positions.IN_PROMPT;
            power_user.persona_description_depth = DEFAULT_DEPTH;
            power_user.persona_description_role = DEFAULT_ROLE;
            power_user.persona_description_lorebook = '';
            power_user.persona_descriptions[user_avatar] = {
                description: '',
                position: persona_description_positions.IN_PROMPT,
                depth: DEFAULT_DEPTH,
                role: DEFAULT_ROLE,
                lorebook: '',
                connections: [],
                title: '',
            };
        }

        setPersonaDescription();

        // Update the locked persona if setting is enabled
        if (shouldAutoLock) {
            chat_metadata.persona = user_avatar;
            console.log(`Auto locked persona to ${user_avatar}`);
            if (toastPersonaNameChange && power_user.persona_show_notifications) {
                toastr.success(t`Persona ${personaName} selected and auto-locked to current chat`, t`Persona Selected`);
            }
            saveMetadataDebounced();
            updatePersonaUIStates();
        }

        // As the last step, inform user if the persona is only temporarily chosen
        if (power_user.persona_show_notifications && !isPersonaPanelOpen()) {
            const temporary = getPersonaTemporaryLockInfo();
            if (temporary.isTemporary) {
                toastr.info(t`This persona is only temporarily chosen. Click for more info.`, t`Temporary Persona`, {
                    preventDuplicates: true,
                    onclick: () => {
                        toastr.info(escapeHtml(temporary.info).replaceAll('\n', '<br />'), t`Temporary Persona`, { escapeHtml: false });
                    },
                });
            }
        }
    }
}

/**
 * Checks if a connection is locked for the current character or group edit menu
 * @param {PersonaConnection} connection - Connection to check
 * @returns {boolean} Whether the connection is locked
 */
export function isPersonaConnectionLocked(connection) {
    return (!selected_group && connection.type === 'character' && connection.id === characters[this_chid]?.avatar)
        || (selected_group && connection.type === 'group' && connection.id === selected_group);
}

/**
 * Checks if the persona is locked
 * @param {PersonaLockType} type - Lock type
 * @returns {boolean} Whether the persona is locked
 */
export function isPersonaLocked(type = 'chat') {
    switch (type) {
        case 'default':
            return power_user.default_persona === user_avatar;
        case 'chat':
            return chat_metadata.persona == user_avatar;
        case 'character': {
            return !!power_user.persona_descriptions[user_avatar]?.connections?.some(isPersonaConnectionLocked);
        }
        default: throw new Error(`Unknown persona lock type: ${type}`);
    }
}

/**
 * Locks or unlocks the persona
 * @param {boolean} state Desired lock state
 * @param {PersonaLockType} type - Lock type
 * @returns {Promise<void>}
 */
export async function setPersonaLockState(state, type = 'chat') {
    return state ? await lockPersona(type) : await unlockPersona(type);
}

/**
 * Toggle the persona lock state
 * @param {PersonaLockType} type - Lock type
 * @returns {Promise<boolean>} - Whether the persona was locked
 */
export async function togglePersonaLock(type = 'chat') {
    if (isPersonaLocked(type)) {
        await unlockPersona(type);
        return false;
    } else {
        await lockPersona(type);
        return true;
    }
}

/**
 * Unlock the persona
 * @param {PersonaLockType} type - Lock type
 * @returns {Promise<void>}
 */
async function unlockPersona(type = 'chat') {
    switch (type) {
        case 'default': {
            // TODO: Make this toggle-able
            await toggleDefaultPersona(user_avatar, { quiet: true });
            break;
        }
        case 'chat': {
            if (chat_metadata.persona) {
                console.log(`Unlocking persona ${user_avatar} from this chat`);
                delete chat_metadata.persona;
                await saveMetadata();
                if (power_user.persona_show_notifications && !isPersonaPanelOpen()) {
                    toastr.info(t`Persona ${name1} is now unlocked from this chat.`, t`Persona Unlocked`);
                }
            }
            break;
        }
        case 'character': {
            /** @type {PersonaConnection[]} */
            const connections = power_user.persona_descriptions[user_avatar]?.connections;
            if (connections) {
                console.log(`Unlocking persona ${user_avatar} from this character ${name2}`);
                power_user.persona_descriptions[user_avatar].connections = connections.filter(c => !isPersonaConnectionLocked(c));
                saveSettingsDebounced();
                updatePersonaConnectionsAvatarList();
                if (power_user.persona_show_notifications && !isPersonaPanelOpen()) {
                    toastr.info(t`Persona ${name1} is now unlocked from character ${name2}.`, t`Persona Unlocked`);
                }
            }
            break;
        }
        default:
            throw new Error(`Unknown persona lock type: ${type}`);
    }

    updatePersonaUIStates();
}

/**
 * Lock the persona
 * @param {PersonaLockType} type - Lock type
 */
async function lockPersona(type = 'chat') {
    // First make sure that user_avatar is actually a persona
    if (!(user_avatar in power_user.personas)) {
        console.log(`Creating a new persona ${user_avatar}`);
        if (power_user.persona_show_notifications) {
            toastr.info(t`Creating a new persona for currently selected user name and avatar...`, t`Persona Not Found`);
        }
        power_user.personas[user_avatar] = name1;
        power_user.persona_descriptions[user_avatar] = {
            description: '',
            position: persona_description_positions.IN_PROMPT,
            depth: DEFAULT_DEPTH,
            role: DEFAULT_ROLE,
            lorebook: '',
            connections: [],
            title: '',
        };
        await eventSource.emit(event_types.PERSONA_CREATED, { avatarId: user_avatar, name: name1, description: '', title: '' });
    }

    switch (type) {
        case 'default': {
            await toggleDefaultPersona(user_avatar, { quiet: true });
            break;
        }
        case 'chat': {
            console.log(`Locking persona ${user_avatar} to this chat`);
            chat_metadata.persona = user_avatar;
            saveMetadataDebounced();
            if (power_user.persona_show_notifications && !isPersonaPanelOpen()) {
                toastr.success(t`User persona ${name1} is locked to ${name2} in this chat`, t`Persona Locked`);
            }
            break;
        }
        case 'character': {
            const newConnection = getCurrentConnectionObj();
            /** @type {PersonaConnection[]} */
            const connections = power_user.persona_descriptions[user_avatar].connections?.filter(c => !isPersonaConnectionLocked(c)) ?? [];
            if (newConnection && newConnection.id) {
                console.log(`Locking persona ${user_avatar} to this character ${name2}`);
                power_user.persona_descriptions[user_avatar].connections = [...connections, newConnection];

                const unlinkedCharacters = [];
                if (!power_user.persona_allow_multi_connections) {
                    for (const [avatarId, description] of Object.entries(power_user.persona_descriptions)) {
                        if (avatarId === user_avatar) continue;

                        const filteredConnections = description.connections?.filter(c => !(c.type === newConnection.type && c.id === newConnection.id)) ?? [];
                        if (filteredConnections.length !== description.connections?.length) {
                            description.connections = filteredConnections;
                            unlinkedCharacters.push(power_user.personas[avatarId]);
                        }
                    }
                }

                saveSettingsDebounced();
                updatePersonaConnectionsAvatarList();
                if (power_user.persona_show_notifications) {
                    let additional = '';
                    if (unlinkedCharacters.length)
                        additional += `<br /><br />${t`Unlinked existing persona${unlinkedCharacters.length > 1 ? 's' : ''}: ${unlinkedCharacters.map(escapeHtml).join(', ')}`}`;
                    if (additional || !isPersonaPanelOpen()) {
                        toastr.success(t`User persona ${escapeHtml(name1)} is locked to character ${escapeHtml(name2)}${additional}`, t`Persona Locked`, { escapeHtml: false });
                    }
                }
            }
            break;
        }
        default:
            throw new Error(`Unknown persona lock type: ${type}`);
    }

    updatePersonaUIStates();
}


/**
 * Click handler for the delete persona button. Delegates to deletePersona with the current user avatar.
 */
async function deleteUserAvatar() {
    await deletePersona(user_avatar);
}

/**
 * Deletes a persona by avatar id.
 * @param {string} avatarId The persona's avatar id to delete
 * @param {object} [options] Options
 * @param {boolean} [options.silent=false] If true, skips the confirmation popup and suppresses toast notifications
 * @returns {Promise<boolean>} True if the persona was deleted
 */
async function deletePersona(avatarId, { silent = false } = {}) {
    if (!avatarId) {
        console.warn('No avatar id found');
        return false;
    }

    const name = power_user.personas[avatarId] || '';

    if (!silent) {
        const confirm = await Popup.show.confirm(
            t`Delete Persona` + `: ${name}`,
            t`Are you sure you want to delete this avatar?` + '<br />' + t`All information associated with its linked persona will be lost.`);

        if (!confirm) {
            console.debug('User cancelled deleting avatar');
            return false;
        }
    }

    const request = await fetch('/api/avatars/delete', {
        method: 'POST',
        headers: getRequestHeaders(),
        body: JSON.stringify({
            'avatar': avatarId,
        }),
    });

    if (request.ok) {
        console.log(`Deleted avatar ${avatarId}`);
        delete power_user.personas[avatarId];
        delete power_user.persona_descriptions[avatarId];

        if (avatarId === power_user.default_persona) {
            if (!silent) toastr.warning(t`The default persona was deleted. You will need to set a new default persona.`, t`Default Persona Deleted`);
            power_user.default_persona = null;
        }

        if (avatarId === chat_metadata.persona) {
            if (!silent) toastr.warning(t`The locked persona was deleted. You will need to set a new persona for this chat.`, t`Persona Deleted`);
            delete chat_metadata.persona;
            await saveMetadata();
        }

        saveSettingsDebounced();
        await eventSource.emit(event_types.PERSONA_DELETED, { avatarId, name });

        // Use the existing mechanism to re-render the persona list and choose the next persona here
        personaLastLoadedChatId = uuidv4(); // Force reload by making a dummy chat id
        await loadPersonaForCurrentChat({ doRender: true });
        return true;
    }

    return false;
}

async function onPersonaDescriptionInput() {
    power_user.persona_description = String($('#persona_description').val());
    countPersonaDescriptionTokens();

    if (power_user.personas[user_avatar]) {
        let object = power_user.persona_descriptions[user_avatar];

        if (!object) {
            object = {
                description: power_user.persona_description,
                position: Number($('#persona_description_position').find(':selected').val()),
                depth: Number($('#persona_depth_value').val()),
                role: Number($('#persona_depth_role').find(':selected').val()),
                lorebook: '',
                title: '',
            };
            power_user.persona_descriptions[user_avatar] = object;
        }

        object.description = power_user.persona_description;
    }

    $(`.avatar-container[data-avatar-id="${user_avatar}"] .ch_description`)
        .text(power_user.persona_description || $('#user_avatar_block').attr('no_desc_text'))
        .toggleClass('text_muted', !power_user.persona_description);
    saveSettingsDebounced();

    if (power_user.personas[user_avatar]) {
        await eventSource.emit(event_types.PERSONA_UPDATED, user_avatar);
    }
}

async function onPersonaDescriptionDepthValueInput() {
    power_user.persona_description_depth = Number($('#persona_depth_value').val());

    if (power_user.personas[user_avatar]) {
        const object = getOrCreatePersonaDescriptor();
        object.depth = power_user.persona_description_depth;
        saveSettingsDebounced();
        await eventSource.emit(event_types.PERSONA_UPDATED, user_avatar);
        return;
    }

    saveSettingsDebounced();
}

async function onPersonaDescriptionDepthRoleInput() {
    power_user.persona_description_role = Number($('#persona_depth_role').find(':selected').val());

    if (power_user.personas[user_avatar]) {
        const object = getOrCreatePersonaDescriptor();
        object.role = power_user.persona_description_role;
        saveSettingsDebounced();
        await eventSource.emit(event_types.PERSONA_UPDATED, user_avatar);
        return;
    }

    saveSettingsDebounced();
}

/**
 * Opens a popup to set the lorebook for the current persona.
 * @param {Pick<JQuery.ClickEvent, 'shiftKey' | 'altKey'>} event Click event
 */
async function onPersonaLoreButtonClick({ shiftKey, altKey }) {
    const personaName = power_user.personas[user_avatar];
    const selectedLorebook = power_user.persona_description_lorebook;

    if (!personaName) {
        toastr.warning(t`You must bind a name to this persona before you can set a lorebook.`, t`Persona Name Not Set`);
        return;
    }

    if (selectedLorebook && !shiftKey && !altKey) {
        openWorldInfoEditor(selectedLorebook);
        return;
    }

    const template = $(await renderTemplateAsync('personaLorebook'));

    const worldSelect = template.find('select');
    template.find('.persona_name').text(personaName);

    for (const worldName of world_names) {
        const option = document.createElement('option');
        option.value = worldName;
        option.innerText = worldName;
        option.selected = selectedLorebook === worldName;
        worldSelect.append(option);
    }

    worldSelect.on('change', async function () {
        power_user.persona_description_lorebook = String($(this).val());

        if (power_user.personas[user_avatar]) {
            const object = getOrCreatePersonaDescriptor();
            object.lorebook = power_user.persona_description_lorebook;
        }

        $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook);
        saveSettingsDebounced();

        if (power_user.personas[user_avatar]) {
            await eventSource.emit(event_types.PERSONA_UPDATED, user_avatar);
        }
    });

    await callGenericPopup(template, POPUP_TYPE.TEXT);
}

async function onPersonaDescriptionPositionInput() {
    power_user.persona_description_position = Number(
        $('#persona_description_position').find(':selected').val(),
    );

    if (power_user.personas[user_avatar]) {
        const object = getOrCreatePersonaDescriptor();
        object.position = power_user.persona_description_position;
        saveSettingsDebounced();
        await eventSource.emit(event_types.PERSONA_UPDATED, user_avatar);
        $('#persona_depth_position_settings').toggle(power_user.persona_description_position === persona_description_positions.AT_DEPTH);
        return;
    }

    saveSettingsDebounced();
    $('#persona_depth_position_settings').toggle(power_user.persona_description_position === persona_description_positions.AT_DEPTH);
}

export function getOrCreatePersonaDescriptor() {
    let object = power_user.persona_descriptions[user_avatar];

    if (!object) {
        object = {
            description: power_user.persona_description,
            position: power_user.persona_description_position,
            depth: power_user.persona_description_depth,
            role: power_user.persona_description_role,
            lorebook: power_user.persona_description_lorebook,
            connections: [],
            title: '',
        };
        power_user.persona_descriptions[user_avatar] = object;
    }
    return object;
}

/**
 * Sets a persona as the default one to be used for all new chats and unlocked existing chats
 * @param {string} avatarId The avatar id of the persona to set as the default
 * @param {object} [options] Optional arguments
 * @param {boolean} [options.quiet=false] If true, no confirmation popups will be shown
 * @returns {Promise<void>}
 */
async function toggleDefaultPersona(avatarId, { quiet = false } = {}) {
    if (!avatarId) {
        console.warn('No avatar id found');
        return;
    }

    const currentDefault = power_user.default_persona;

    if (power_user.personas[avatarId] === undefined) {
        console.warn(`No persona name found for avatar ${avatarId}`);
        toastr.warning(t`You must bind a name to this persona before you can set it as the default.`, t`Persona Name Not Set`);
        return;
    }


    if (avatarId === currentDefault) {
        if (!quiet) {
            const confirm = await Popup.show.confirm(t`Are you sure you want to remove the default persona?`, power_user.personas[avatarId]);
            if (!confirm) {
                console.debug('User cancelled removing default persona');
                return;
            }
        }

        console.log(`Removing default persona ${avatarId}`);
        if (power_user.persona_show_notifications && !isPersonaPanelOpen()) {
            toastr.info(t`This persona will no longer be used by default when you open a new chat.`, t`Default Persona Removed`);
        }
        delete power_user.default_persona;
    } else {
        if (!quiet) {
            const confirm = await Popup.show.confirm(t`Set Default Persona`,
                t`Are you sure you want to set \"${power_user.personas[avatarId]}\" as the default persona?`
                + '<br /><br />'
                + t`This name and avatar will be used for all new chats, as well as existing chats where the user persona is not locked.`);
            if (!confirm) {
                console.debug('User cancelled setting default persona');
                return;
            }
        }

        power_user.default_persona = avatarId;
        if (power_user.persona_show_notifications && !isPersonaPanelOpen()) {
            toastr.success(t`Set to ${power_user.personas[avatarId]}.This persona will be used by default when you open a new chat.`, t`Default Persona`);
        }
    }

    saveSettingsDebounced();
    await getUserAvatars(true, avatarId);
    updatePersonaUIStates();
}

/**
 * Returns an object with 3 properties that describe the state of the given persona
 *
 * - default: Whether this persona is the default one for all new chats
 * - locked: An object containing the lock states
 *   - chat: Whether the persona is locked to the currently open chat
 *   - character: Whether the persona is locked to the currently open character or group
 * @param {string} avatarId - The avatar id of the persona to get the state for
 * @returns {PersonaState} An object describing the state of the given persona
 */
function getPersonaStates(avatarId) {
    const isDefaultPersona = power_user.default_persona === avatarId;
    const hasChatLock = chat_metadata.persona == avatarId;

    /** @type {PersonaConnection[]} */
    const connections = power_user.persona_descriptions[avatarId]?.connections;
    const hasCharLock = !!connections?.some(c =>
        (!selected_group && c.type === 'character' && c.id === characters[Number(this_chid)]?.avatar)
        || (selected_group && c.type === 'group' && c.id === selected_group));

    return {
        avatarId: avatarId,
        default: isDefaultPersona,
        locked: {
            chat: hasChatLock,
            character: hasCharLock,
        },
    };
}

/**
 * Updates the UI to reflect the current states of all personas and the selected user's persona.
 * This includes updating class states on avatar containers to indicate default status, chat lock,
 * and character lock, as well as updating icons and labels in the persona management panel to reflect
 * the current state of the user's persona.
 * Additionally, it manages the display of temporary persona lock information.
 * @param {Object} [options={}] - Optional settings
 * @param {boolean} [options.navigateToCurrent=false] - Whether to navigate to the current persona in the persona list
 */

function updatePersonaUIStates({ navigateToCurrent = false } = {}) {
    if (navigateToCurrent) {
        navigateToAvatar(user_avatar);
    }

    // Update the persona list
    $('#user_avatar_block .avatar-container').each(function () {
        const avatarId = $(this).attr('data-avatar-id');
        const states = getPersonaStates(avatarId);
        $(this).toggleClass('default_persona', states.default);
        $(this).toggleClass('locked_to_chat', states.locked.chat);
        $(this).toggleClass('locked_to_character', states.locked.character);
        $(this).toggleClass('selected', avatarId === user_avatar);
    });

    // Buttons for the persona panel on the right
    const personaStates = getPersonaStates(user_avatar);

    $('#lock_persona_default').toggleClass('locked', personaStates.default);

    $('#lock_user_name').toggleClass('locked', personaStates.locked.chat);
    $('#lock_user_name i.icon').toggleClass('fa-lock', personaStates.locked.chat);
    $('#lock_user_name i.icon').toggleClass('fa-unlock', !personaStates.locked.chat);

    $('#lock_persona_to_char').toggleClass('locked', personaStates.locked.character);
    $('#lock_persona_to_char i.icon').toggleClass('fa-lock', personaStates.locked.character);
    $('#lock_persona_to_char i.icon').toggleClass('fa-unlock', !personaStates.locked.character);

    // Persona panel info block
    const { isTemporary, info } = getPersonaTemporaryLockInfo();
    if (isTemporary) {
        const messageContainer = document.createElement('div');
        const messageSpan = document.createElement('span');
        messageSpan.textContent = t`Temporary persona in use.`;
        messageContainer.appendChild(messageSpan);
        messageContainer.classList.add('flex-container', 'alignItemsBaseline');

        const infoIcon = document.createElement('i');
        infoIcon.classList.add('fa-solid', 'fa-circle-info', 'opacity50p');
        infoIcon.title = info;
        messageContainer.appendChild(infoIcon);

        // Set the info block content
        setInfoBlock('#persona_connections_info_block', messageContainer, 'hint');
    } else {
        // Clear the info block if no condition applies
        clearInfoBlock('#persona_connections_info_block');
    }
}

/**
 * @typedef {Object} PersonaLockInfo
 * @property {boolean} isTemporary - Whether the selected persona is temporary based on current locks.
 * @property {boolean} hasDifferentChatLock - True if the chat persona is set and differs from the user avatar.
 * @property {boolean} hasDifferentDefaultLock - True if the default persona is set and differs from the user avatar.
 * @property {string} info - Detailed information about the current, chat, and default personas.
 */

/**
 * Computes temporary lock information for the current persona.
 *
 * This function checks whether the currently selected persona is temporary by comparing
 * the chat persona and the default persona to the user avatar. If either is different,
 * the currently selected persona is considered temporary and a detailed message is generated.
 *
 * @returns {PersonaLockInfo} An object containing flags and a message describing the persona lock status.
 */
function getPersonaTemporaryLockInfo() {
    const hasDifferentChatLock = !!chat_metadata.persona && chat_metadata.persona !== user_avatar;
    const hasDifferentDefaultLock = power_user.default_persona && power_user.default_persona !== user_avatar;
    const isTemporary = hasDifferentChatLock || (!chat_metadata.persona && hasDifferentDefaultLock);
    const info = isTemporary ? t`A different persona is locked to this chat, or you have a different default persona set. The currently selected persona will only be temporary, and resets on reload. Consider locking this persona to the chat if you want to permanently use it.`
        + '\n\n'
        + t`Current Persona: ${power_user.personas[user_avatar]}`
        + (hasDifferentChatLock ? '\n' + t`Chat persona: ${power_user.personas[chat_metadata.persona]}` : '')
        + (hasDifferentDefaultLock ? '\n' + t`Default persona: ${power_user.personas[power_user.default_persona]}` : '') : '';

    return {
        isTemporary: isTemporary,
        hasDifferentChatLock: hasDifferentChatLock,
        hasDifferentDefaultLock: hasDifferentDefaultLock,
        info: info,
    };
}

/**
 * Loads the appropriate persona for the current chat session based on locks (chat lock, char lock, default persona)
 *
 * @param {Object} [options={}] - Optional arguments
 * @param {boolean} [options.doRender=false] - Whether to render the persona immediately
 * @returns {Promise<boolean>} - A promise that resolves to a boolean indicating whether a persona was selected
 */
async function loadPersonaForCurrentChat({ doRender = false } = {}) {
    const currentChatId = getCurrentChatId();
    if (currentChatId === personaLastLoadedChatId) return;
    personaLastLoadedChatId = currentChatId;

    // Cache persona list to check if they exist
    const userAvatars = await getUserAvatars(doRender);

    // Check if the user avatar is set and exists in the list of user avatars
    if (userAvatars.length && !userAvatars.includes(user_avatar)) {
        console.log(`User avatar ${user_avatar} not found in user avatars list, pick the first available one`);
        await setUserAvatar(userAvatars[0], { toastPersonaNameChange: false, navigateToCurrent: true });
    }

    // Define a persona for this chat
    let chatPersona = '';

    /** @type {'chat' | 'character' | 'default' | null} */
    let connectType = null;

    // If persona is locked in chat metadata, select it
    if (chat_metadata.persona) {
        console.log(`Using locked persona ${chat_metadata.persona}`);
        chatPersona = chat_metadata.persona;

        // Verify it exists
        if (!userAvatars.includes(chatPersona)) {
            console.warn('Chat-locked persona avatar not found, unlocking persona');
            delete chat_metadata.persona;
            saveSettingsDebounced();
            chatPersona = '';
        }
        if (chatPersona) connectType = 'chat';
    }

    // If the persona panel is open when the chat changes, this is likely because a character was selected from that panel.
    // In that case, we are not automatically switching persona - but need to make changes if there is any chat-bound connection
    /*
    if (isPersonaPanelOpen()) {
        if (chatPersona) {
            // If the chat-bound persona is the currently selected one, we can simply exit out
            if (chatPersona === user_avatar) {
                return false;
            }
            // Otherwise ask if we want to switch
            const autoLock = power_user.persona_auto_lock;
            const result = await Popup.show.confirm(t`Switch Persona?`,
                t`You have a connected persona for the current chat (${power_user.personas[chatPersona]}). Do you want to stick to the current persona (${power_user.personas[user_avatar]}) ${(autoLock ? t`and lock that to the chat` : '')}, or switch to ${power_user.personas[chatPersona]} instead?`,
                { okButton: autoLock ? t`Keep and Lock` : t`Keep`, cancelButton: t`Switch` });
            if (result === POPUP_RESULT.AFFIRMATIVE) {
                if (autoLock) {
                    lockPersona('chat');
                }
                return false;
            }
        } else {
            // If we don't have a chat-bound persona, we simply return and keep the current one we have
            return false;
        }
    }
    */

    // Check if we have any persona connected to the current character
    if (!chatPersona) {
        const connectedPersonas = getConnectedPersonas();

        if (connectedPersonas.length > 0) {
            if (connectedPersonas.length === 1) {
                chatPersona = connectedPersonas[0];
            } else if (!power_user.persona_allow_multi_connections) {
                console.warn('More than one persona is connected to this character.Using the first available persona for this chat.');
                chatPersona = connectedPersonas[0];
            } else {
                chatPersona = await askForPersonaSelection(t`Select Persona`,
                    t`Multiple personas are connected to this character.\nSelect a persona to use for this chat.`,
                    connectedPersonas, { highlightPersonas: true, targetedChar: getCurrentConnectionObj() });
            }
        }

        if (chatPersona) connectType = 'character';
    }

    // Last check if default persona is set, select it
    if (!chatPersona && power_user.default_persona) {
        console.log(`Using default persona ${power_user.default_persona}`);
        chatPersona = power_user.default_persona;

        if (chatPersona) connectType = 'default';
    }

    // Whatever way we selected a persona, if it doesn't exist, unlock this chat
    if (chat_metadata.persona && !userAvatars.includes(chat_metadata.persona)) {
        console.warn('Persona avatar not found, unlocking persona');
        delete chat_metadata.persona;
    }

    // Default persona missing
    if (power_user.default_persona && !userAvatars.includes(power_user.default_persona)) {
        console.warn('Default persona avatar not found, clearing default persona');
        power_user.default_persona = null;
        saveSettingsDebounced();
    }

    // Persona avatar found, select it
    if (chatPersona && user_avatar !== chatPersona) {
        const willAutoLock = power_user.persona_auto_lock && user_avatar !== chat_metadata.persona;
        await setUserAvatar(chatPersona, { toastPersonaNameChange: false, navigateToCurrent: true });

        if (power_user.persona_show_notifications) {
            let message = t`Auto-selected persona based on ${connectType} connection.<br />Your messages will now be sent as ${power_user.personas[chatPersona]}.`;
            if (willAutoLock) {
                message += '<br /><br />' + t`Auto-locked this persona to current chat.`;
            }
            toastr.success(message, t`Persona Auto Selected`, { escapeHtml: false });
        }
    } else if (chatPersona && power_user.persona_auto_lock && !chat_metadata.persona) {
        // Even if it's the same persona, we still might need to auto-lock to chat if that's enabled
        await lockPersona('chat');
    }

    updatePersonaUIStates();

    return !!chatPersona;
}

/**
 * Returns an array of persona keys that are connected to the given character key.
 * If the character key is not provided, it defaults to the currently selected group or character.
 * @param {string} [characterKey] - The character key to query
 * @returns {string[]} - An array of persona keys that are connected to the given character key
 */
export function getConnectedPersonas(characterKey = undefined) {
    characterKey ??= selected_group || characters[Number(this_chid)]?.avatar;
    const connectedPersonas = Object.entries(power_user.persona_descriptions)
        .filter(([_, { connections }]) => connections?.some(conn => conn.id === characterKey))
        .map(([key, _]) => key);
    return connectedPersonas;
}


/**
 * Shows a popup with all personas connected to the currently selected character or group.
 * In the popup, the user can select a persona to load for the current character or group, or shift-click to remove the connection.
 * @return {Promise<void>}
 */
export async function showCharConnections() {
    let isRemoving = false;

    const connections = getConnectedPersonas();
    const message = t`The following personas are connected to the current character.\n\nClick on a persona to select it for the current character.\nShift + Click to unlink the persona from the character.`;
    const selectedPersona = await askForPersonaSelection(t`Persona Connections`, message, connections, {
        okButton: t`Ok`,
        highlightPersonas: true,
        targetedChar: getCurrentConnectionObj(),
        shiftClickHandler: (element, ev) => {
            const personaId = $(element).attr('data-pid');

            /** @type {PersonaConnection[]} */
            const connections = power_user.persona_descriptions[personaId]?.connections;
            if (connections) {
                console.log(`Unlocking persona ${personaId} from current character ${name2}`);
                power_user.persona_descriptions[personaId].connections = connections.filter(c => {
                    if (menu_type == 'group_edit' && c.type == 'group' && c.id == selected_group) return false;
                    else if (c.type == 'character' && c.id == characters[Number(this_chid)]?.avatar) return false;
                    return true;
                });
                saveSettingsDebounced();
                updatePersonaConnectionsAvatarList();
                if (power_user.persona_show_notifications) {
                    toastr.info(t`User persona ${power_user.personas[personaId]} is now unlocked from the current character ${name2}.`, t`Persona unlocked`);
                }

                isRemoving = true;
                $('#char_connections_button').trigger('click');
            }
        },
    });

    // One of the persona was selected. So load it.
    if (!isRemoving && selectedPersona) {
        await setUserAvatar(selectedPersona, { toastPersonaNameChange: false });
        if (power_user.persona_show_notifications) {
            toastr.success(t`Selected persona ${power_user.personas[selectedPersona]} for current chat.`, t`Connected Persona Selected`);
        }
    }
}

/**
 * Retrieves the current connection object based on whether the current chat is with a char or a group.
 *
 * @returns {PersonaConnection} An object representing the current connection
 */
export function getCurrentConnectionObj() {
    if (selected_group)
        return { type: 'group', id: selected_group };
    if (characters[Number(this_chid)]?.avatar)
        return { type: 'character', id: characters[Number(this_chid)]?.avatar };
    return null;
}

function onBackupPersonas() {
    const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, '');
    const filename = `personas_${timestamp}.json`;
    const data = JSON.stringify({
        'personas': power_user.personas,
        'persona_descriptions': power_user.persona_descriptions,
        'default_persona': power_user.default_persona,
    }, null, 2);

    const blob = new Blob([data], { type: 'application/json' });
    download(blob, filename, 'application/json');
}

async function onPersonasRestoreInput(e) {
    const file = e.target.files[0];

    if (!file) {
        console.debug('No file selected');
        return;
    }

    const data = await parseJsonFile(file);

    if (!data) {
        toastr.warning(t`Invalid file selected`, t`Persona Management`);
        console.debug('Invalid file selected');
        return;
    }

    if (!data.personas || !data.persona_descriptions || typeof data.personas !== 'object' || typeof data.persona_descriptions !== 'object') {
        toastr.warning(t`Invalid file format`, t`Persona Management`);
        console.debug('Invalid file selected');
        return;
    }

    const avatarsList = await getUserAvatars(false);
    const warnings = [];

    // Merge personas with existing ones
    for (const [key, value] of Object.entries(data.personas)) {
        if (key in power_user.personas) {
            warnings.push(`Persona "${key}" (${value}) already exists, skipping`);
            continue;
        }

        power_user.personas[key] = value;

        // If the avatar is missing, upload it
        if (!avatarsList.includes(key)) {
            warnings.push(`Persona image "${key}" (${value}) is missing, uploading default avatar`);
            await uploadUserAvatar(default_user_avatar, key);
        }
    }

    // Merge persona descriptions with existing ones
    for (const [key, value] of Object.entries(data.persona_descriptions)) {
        if (key in power_user.persona_descriptions) {
            warnings.push(`Persona description for "${key}" (${power_user.personas[key]}) already exists, skipping`);
            continue;
        }

        if (!power_user.personas[key]) {
            warnings.push(`Persona for "${key}" does not exist, skipping`);
            continue;
        }

        power_user.persona_descriptions[key] = value;
    }

    if (data.default_persona) {
        if (data.default_persona in power_user.personas) {
            power_user.default_persona = data.default_persona;
        } else {
            warnings.push(`Default persona "${data.default_persona}" does not exist, skipping`);
        }
    }

    if (warnings.length) {
        toastr.success(t`Personas restored with warnings. Check console for details.`, t`Persona Management`);
        console.warn(`PERSONA RESTORE REPORT\n====================\n${warnings.join('\n')}`);
    } else {
        toastr.success(t`Personas restored successfully.`, t`Persona Management`);
    }

    await getUserAvatars();
    setPersonaDescription();
    saveSettingsDebounced();
    $('#personas_restore_input').val('');
}

/**
 * Synchronizes user-sent messages in the chat to the current persona.
 * @param {object} [options={}] - Optional parameters
 * @param {number} [options.start=0] - Start index of the message range (inclusive)
 * @param {number} [options.end=chat.length - 1] - End index of the message range (inclusive)
 * @param {boolean} [options.quiet=false] - If true, skips the confirmation popup
 * @param {string} [options.nameFilter=''] - Filter messages by name (case-insensitive)
 * @returns {Promise<void>}
 */
async function syncUserNameToPersona({ start = 0, end = chat.length - 1, quiet = false, nameFilter = '' } = {}) {
    const isRangeAll = start === 0 && end === chat.length - 1;
    const hasNameFilter = nameFilter?.trim();
    const confirmMessage = isRangeAll && !hasNameFilter
        ? t`All user-sent messages in this chat will be attributed to ${name1}.`
        : isRangeAll && hasNameFilter
            ? t`User-sent messages with name "${nameFilter}" will be attributed to ${name1}.`
            : !isRangeAll && !hasNameFilter
                ? t`User-sent messages in the specified range will be attributed to ${name1}.`
                : t`User-sent messages with name "${nameFilter}" in the specified range will be attributed to ${name1}.`;

    if (!quiet) {
        const confirmation = await Popup.show.confirm(t`Are you sure?`, confirmMessage);
        if (!confirmation) {
            return;
        }
    }

    for (let i = start; i <= end; i++) {
        const mes = chat[i];
        if (mes?.is_user && (!hasNameFilter || equalsIgnoreCaseAndAccents(mes.name, nameFilter))) {
            mes.name = name1;
            mes.force_avatar = getThumbnailUrl('persona', user_avatar);
        }
    }

    await saveChatConditional();
    await reloadCurrentChat();
}

/**
 * Retriggers the first message to reload it from the char definition.
 */
export async function retriggerFirstMessageOnEmptyChat() {
    if (chat_metadata.tainted) {
        return;
    }
    if (selected_group) {
        await reloadCurrentChat();
    }
    if (!selected_group && Number(this_chid) >= 0 && chat.length === 1) {
        await createOrEditCharacter();
    }
}

/**
 * Duplicates a persona.
 * @param {string} avatarId Source persona avatar id
 * @param {object} [options] Options
 * @param {boolean} [options.silent=false] If true, skips the confirmation popup
 * @param {boolean} [options.select=false] If true, selects/activates the duplicated persona
 * @returns {Promise<string>} The avatar id of the new persona, or empty string on failure/cancellation
 */
async function duplicatePersona(avatarId, { silent = false, select = false } = {}) {
    const personaName = power_user.personas[avatarId];

    if (!personaName) {
        toastr.warning(t`Chosen avatar is not a persona`, t`Persona Management`);
        return '';
    }

    if (!silent) {
        const confirm = await Popup.show.confirm(t`Are you sure you want to duplicate this persona?`, personaName);

        if (!confirm) {
            console.debug('User cancelled duplicating persona');
            return '';
        }
    }

    const newAvatarId = `${Date.now()}-${personaName.replace(/[^a-zA-Z0-9]/g, '')}.png`;
    const descriptor = power_user.persona_descriptions[avatarId];

    power_user.personas[newAvatarId] = personaName;
    power_user.persona_descriptions[newAvatarId] = {
        description: descriptor?.description ?? '',
        position: descriptor?.position ?? persona_description_positions.IN_PROMPT,
        depth: descriptor?.depth ?? DEFAULT_DEPTH,
        role: descriptor?.role ?? DEFAULT_ROLE,
        lorebook: descriptor?.lorebook ?? '',
        title: descriptor?.title ?? '',
    };

    await uploadUserAvatar(getUserAvatar(avatarId), newAvatarId);

    const eventData = {
        avatarId: newAvatarId,
        name: personaName,
        description: descriptor?.description ?? '',
        title: descriptor?.title ?? '',
        duplicatedFromAvatarId: avatarId,
    };
    await eventSource.emit(event_types.PERSONA_CREATED, eventData);

    await getUserAvatars(true, newAvatarId);
    saveSettingsDebounced();

    if (select) {
        await setUserAvatar(newAvatarId);
    }

    return newAvatarId;
}

/**
 * If a current user avatar is not bound to persona, bind it.
 */
async function migrateNonPersonaUser() {
    if (user_avatar in power_user.personas) {
        return;
    }

    await initPersona(user_avatar, name1, '', '', { silent: true });
    setPersonaDescription();
    await getUserAvatars(true, user_avatar);
}


// #region Persona CRUD Slash Command Utilities

/**
 * Mapping of human-readable position names to persona_description_positions enum values.
 * @type {Record<string, number>}
 */
const POSITION_NAME_MAP = Object.freeze({
    'inprompt': persona_description_positions.IN_PROMPT,
    'topan': persona_description_positions.TOP_AN,
    'bottoman': persona_description_positions.BOTTOM_AN,
    'atdepth': persona_description_positions.AT_DEPTH,
    'none': persona_description_positions.NONE,
});

/**
 * Mapping of human-readable role names to numeric role values.
 * @type {Record<string, number>}
 */
const ROLE_NAME_MAP = Object.freeze({
    'system': 0,
    'user': 1,
    'assistant': 2,
});

/**
 * Parses a persona description position from a string or number value.
 * @param {string|number|undefined} value Position value (name or number)
 * @returns {number|null} Parsed position value, or null if invalid/undefined
 */
function parsePersonaPosition(value) {
    if (value === undefined || value === null) return null;
    const strValue = String(value).toLowerCase();
    if (strValue in POSITION_NAME_MAP) return POSITION_NAME_MAP[strValue];
    const numValue = Number(value);
    if (!isNaN(numValue) && Object.values(persona_description_positions).includes(numValue)) return numValue;
    return null;
}

/**
 * Parses a persona description role from a string or number value.
 * @param {string|number|undefined} value Role value (name or number)
 * @returns {number|null} Parsed role value, or null if invalid/undefined
 */
function parsePersonaRole(value) {
    if (value === undefined || value === null) return null;
    const strValue = String(value).toLowerCase();
    if (strValue in ROLE_NAME_MAP) return ROLE_NAME_MAP[strValue];
    const numValue = Number(value);
    if (!isNaN(numValue) && numValue >= 0 && numValue <= 2) return numValue;
    return null;
}

/**
 * Uploads base64 avatar data to a persona, optionally showing a crop dialog.
 * @param {string} avatarId The persona's avatar file name
 * @param {string} base64Data Base64 data URL of the image
 * @param {object} [options] Options
 * @param {boolean} [options.resizePrompt=false] Whether to show the crop dialog
 * @returns {Promise<boolean>} True if upload was successful
 */
async function uploadPersonaAvatar(avatarId, base64Data, { resizePrompt = false } = {}) {
    if (!base64Data || !avatarId) return false;

    let finalImageData = base64Data;

    if (resizePrompt && !power_user.never_resize_avatars) {
        const dlg = new Popup(t`Set the crop position of the avatar image`, POPUP_TYPE.CROP, '', { cropImage: base64Data });
        const croppedImage = await dlg.show();
        if (!croppedImage) return false;
        finalImageData = String(croppedImage);
    }

    try {
        const response = await fetch(finalImageData);
        const blob = await response.blob();
        const file = new File([blob], 'avatar.png', { type: 'image/png' });
        const formData = new FormData();
        formData.append('avatar', file);
        formData.append('overwrite_name', avatarId);

        const uploadResponse = await fetch('/api/avatars/upload', {
            method: 'POST',
            headers: getRequestHeaders({ omitContentType: true }),
            cache: 'no-cache',
            body: formData,
        });

        if (!uploadResponse.ok) {
            throw new Error(`Upload failed: ${uploadResponse.statusText}`);
        }

        // Cache bust for the updated avatar
        await fetch(getUserAvatar(avatarId), { cache: 'reload' });
        await fetch(getThumbnailUrl('persona', avatarId), { cache: 'reload' });
        reloadUserAvatar(true);
        return true;
    } catch (error) {
        console.error('Error uploading persona avatar:', error);
        toastr.warning(t`Failed to upload avatar: ${error.message}`);
        return false;
    }
}

/**
 * Resolves a persona from the given argument or falls back to the currently active persona.
 * @param {string} [personaArg] Persona name or avatar key argument
 * @returns {import('./utils.js').PersonaViewModel|null} The resolved persona, or null if not found
 */
function getTargetPersona(personaArg) {
    if (personaArg) {
        const persona = findPersona({ name: personaArg });
        if (!persona) {
            toastr.warning(t`Persona "${personaArg}" not found`);
            return null;
        }
        return persona;
    }

    // Fall back to currently active persona
    const persona = findPersona({ preferCurrentPersona: true });
    if (!persona) {
        toastr.warning(t`No persona selected and no persona argument provided`);
        return null;
    }
    return persona;
}

// #endregion

// #region Persona CRUD Slash Command Callbacks

/**
 * Creates a new persona with the specified attributes.
 * @param {object} args Named arguments from the slash command
 * @returns {Promise<string>} Avatar key of the created persona, or empty string on failure
 */
async function createPersonaCallback(args) {
    const name = args.name;
    if (!name || typeof name !== 'string' || !name.trim()) {
        toastr.warning(t`Persona name is required`);
        return '';
    }

    const trimmedName = name.trim();
    const avatarId = `${Date.now()}-${trimmedName.replace(/[^a-zA-Z0-9]/g, '')}.png`;

    const description = args.description ?? '';
    const title = args.title ?? '';
    const position = parsePersonaPosition(args.descriptionPosition) ?? persona_description_positions.IN_PROMPT;
    const role = parsePersonaRole(args.descriptionRole) ?? DEFAULT_ROLE;
    const lorebook = args.lorebook ?? '';

    let depth = args.descriptionDepth !== undefined ? Number(args.descriptionDepth) : DEFAULT_DEPTH;
    if (isNaN(depth)) {
        toastr.warning(t`Invalid description depth "${args.descriptionDepth}", defaulting to ${DEFAULT_DEPTH}`);
        depth = DEFAULT_DEPTH;
    }

    // Initialize persona data with all fields
    await initPersona(avatarId, trimmedName, description, title, {
        position, depth, role, lorebook,
    });

    // Handle avatar upload
    const avatarData = args.avatar ? await resolveAvatarData(args.avatar) : null;
    if (avatarData) {
        const resizePrompt = !isFalseBoolean(args.avatarPromptResize ?? 'true');
        const uploaded = await uploadPersonaAvatar(avatarId, avatarData, { resizePrompt });
        if (!uploaded) {
            // Crop was cancelled or upload failed — use default avatar
            await uploadUserAvatar(default_user_avatar, avatarId);
        }
    } else {
        await uploadUserAvatar(default_user_avatar, avatarId);
    }

    saveSettingsDebounced();
    await getUserAvatars(true, avatarId);

    // Select/activate if requested (default: true)
    if (!isFalseBoolean(args.select ?? 'true')) {
        await setUserAvatar(avatarId);
    }

    toastr.success(t`Persona "${trimmedName}" created successfully`);
    return avatarId;
}

/**
 * Updates an existing persona's attributes.
 * @param {object} args Named arguments from the slash command
 * @returns {Promise<string>} Avatar key of the updated persona, or empty string on failure
 */
async function updatePersonaCallback(args) {
    const persona = getTargetPersona(args.persona);
    if (!persona) return '';

    const avatarId = persona.avatar;
    const descriptor = power_user.persona_descriptions[avatarId];

    if (!descriptor) {
        toastr.warning(t`Persona data not found for "${persona.name}"`);
        return '';
    }

    let hasUpdates = false;

    // Update name
    if (args.name !== undefined) {
        const newName = String(args.name).trim();
        if (newName) {
            const oldName = power_user.personas[avatarId];
            power_user.personas[avatarId] = newName;
            if (avatarId === user_avatar) {
                setUserName(newName);
            }
            await eventSource.emit(event_types.PERSONA_RENAMED, { avatarId, oldName, newName });
            hasUpdates = true;
        }
    }

    // Update description
    if (args.description !== undefined) {
        descriptor.description = args.description;
        if (avatarId === user_avatar) {
            power_user.persona_description = args.description;
        }
        hasUpdates = true;
    }

    // Update title
    if (args.title !== undefined) {
        descriptor.title = args.title;
        hasUpdates = true;
    }

    // Update description position
    if (args.descriptionPosition !== undefined) {
        const position = parsePersonaPosition(args.descriptionPosition);
        if (position !== null) {
            descriptor.position = position;
            if (avatarId === user_avatar) {
                power_user.persona_description_position = position;
            }
            hasUpdates = true;
        }
    }

    // Update description depth
    if (args.descriptionDepth !== undefined) {
        const depth = Number(args.descriptionDepth);
        if (!isNaN(depth)) {
            descriptor.depth = depth;
            if (avatarId === user_avatar) {
                power_user.persona_description_depth = depth;
            }
            hasUpdates = true;
        }
    }

    // Update description role
    if (args.descriptionRole !== undefined) {
        const role = parsePersonaRole(args.descriptionRole);
        if (role !== null) {
            descriptor.role = role;
            if (avatarId === user_avatar) {
                power_user.persona_description_role = role;
            }
            hasUpdates = true;
        }
    }

    // Update lorebook
    if (args.lorebook !== undefined) {
        descriptor.lorebook = args.lorebook;
        if (avatarId === user_avatar) {
            power_user.persona_description_lorebook = args.lorebook;
        }
        hasUpdates = true;
    }

    // Handle avatar
    const avatarData = args.avatar ? await resolveAvatarData(args.avatar) : null;
    if (avatarData) {
        const resizePrompt = !isFalseBoolean(args.avatarPromptResize ?? 'true');
        const uploaded = await uploadPersonaAvatar(avatarId, avatarData, { resizePrompt });
        if (uploaded) {
            hasUpdates = true;
        }
    }

    if (!hasUpdates) {
        toastr.info(t`No fields provided to update`);
        return avatarId;
    }

    saveSettingsDebounced();
    await eventSource.emit(event_types.PERSONA_UPDATED, avatarId);

    // Refresh UI if the updated persona is the active one
    if (avatarId === user_avatar) {
        setPersonaDescription();
    }
    await getUserAvatars(true, avatarId);
    updatePersonaUIStates();

    toastr.success(t`Persona "${power_user.personas[avatarId]}" updated successfully`);
    return avatarId;
}

/**
 * Retrieves persona data or a specific field.
 * @param {object} args Named arguments from the slash command
 * @returns {Promise<string>} The persona data or field value
 */
async function getPersonaDataCallback(args) {
    const persona = getTargetPersona(args.persona);
    if (!persona) return '';

    const avatarId = persona.avatar;
    const descriptor = power_user.persona_descriptions[avatarId] ?? {};

    if (args.field) {
        /** @type {Record<string, unknown>} */
        const fieldMap = {
            name: power_user.personas[avatarId] ?? '',
            description: descriptor.description ?? '',
            title: descriptor.title ?? '',
            position: descriptor.position ?? persona_description_positions.IN_PROMPT,
            depth: descriptor.depth ?? DEFAULT_DEPTH,
            role: descriptor.role ?? DEFAULT_ROLE,
            lorebook: descriptor.lorebook ?? '',
            avatar: avatarId,
            default: power_user.default_persona === avatarId,
            connections: descriptor.connections ?? [],
        };

        const value = fieldMap[args.field];
        if (value === undefined) {
            toastr.warning(t`Unknown persona field "${args.field}"`);
            return '';
        }

        return await slashCommandReturnHelper.doReturn(
            args.return ?? 'pipe', value,
            { objectToStringFunc: x => typeof x === 'object' ? JSON.stringify(x) : String(x) },
        );
    }

    // Return full persona data
    const personaData = {
        avatar: avatarId,
        name: power_user.personas[avatarId] ?? '',
        description: descriptor.description ?? '',
        title: descriptor.title ?? '',
        position: descriptor.position ?? persona_description_positions.IN_PROMPT,
        depth: descriptor.depth ?? DEFAULT_DEPTH,
        role: descriptor.role ?? DEFAULT_ROLE,
        lorebook: descriptor.lorebook ?? '',
        default: power_user.default_persona === avatarId,
        connections: descriptor.connections ?? [],
    };

    return await slashCommandReturnHelper.doReturn(
        args.return ?? 'pipe', personaData,
        { objectToStringFunc: x => JSON.stringify(x, null, 2) },
    );
}

/**
 * Deletes a persona via slash command.
 * @param {object} args Named arguments from the slash command
 * @returns {Promise<string>} 'true' if deleted, 'false' otherwise
 */
async function deletePersonaCallback(args) {
    const persona = getTargetPersona(args.persona);
    if (!persona) return 'false';

    const silent = isTrueBoolean(args.silent);
    const success = await deletePersona(persona.avatar, { silent });
    return String(success);
}

/**
 * Duplicates a persona via slash command.
 * @param {object} args Named arguments from the slash command
 * @returns {Promise<string>} Avatar key of the duplicated persona, or empty string on failure
 */
async function duplicatePersonaCallback(args) {
    const persona = getTargetPersona(args.persona);
    if (!persona) return '';

    const shouldSelect = isTrueBoolean(args.select);
    const newAvatarId = await duplicatePersona(persona.avatar, { silent: true, select: shouldSelect });

    if (!newAvatarId) {
        toastr.error(t`Failed to duplicate persona`);
        return '';
    }

    toastr.success(t`Persona "${power_user.personas[newAvatarId]}" duplicated successfully`);
    return newAvatarId;
}

// #endregion

/**
 * Locks or unlocks the persona of the current chat.
 * @param {{type: string}} _args Named arguments
 * @param {string} value The value to set the lock to
 * @returns {Promise<string>} The value of the lock after setting
 */
async function lockPersonaCallback(_args, value) {
    const type = /** @type {PersonaLockType} */ (_args.type ?? 'chat');

    if (!['chat', 'character', 'default'].includes(type)) {
        toastr.warning(t`Unknown lock type "${type}"`, t`Persona Management`);
        return '';
    }

    if (!value) {
        return String(isPersonaLocked(type));
    }

    if (['toggle', 't'].includes(value.trim().toLowerCase())) {
        const result = await togglePersonaLock(type);
        return String(result);
    }

    if (isTrueBoolean(value)) {
        await setPersonaLockState(true, type);
        return 'true';
    }

    if (isFalseBoolean(value)) {
        await setPersonaLockState(false, type);
        return 'false';
    }

    return '';
}

/**
 * Sets a persona name and optionally an avatar.
 * @param {{mode: 'lookup' | 'temp' | 'all'}} namedArgs Named arguments
 * @param {string} name Name to set
 * @returns {Promise<string>}
 */
async function setNameCallback({ mode = 'all' }, name) {
    if (!name) {
        toastr.warning('You must specify a name to change to');
        return '';
    }

    if (!['lookup', 'temp', 'all'].includes(mode)) {
        toastr.warning('Mode must be one of "lookup", "temp" or "all"');
        return '';
    }

    name = name.trim();

    // If the name matches a persona avatar, or a name, auto-select it
    if (['lookup', 'all'].includes(mode)) {
        const persona = findPersona({ name });
        if (persona) {
            await autoSelectPersona(persona.name, { personaKey: persona.avatar });
            return '';
        } else if (mode === 'lookup') {
            toastr.warning(`Persona ${name} not found`);
            return '';
        }
    }

    if (['temp', 'all'].includes(mode)) {
        // Otherwise, set just the name
        setUserName(name); //this prevented quickReply usage
    }

    return '';
}

async function syncCallback(args, value) {
    const range = value ? stringToRange(value, 0, chat.length - 1) : null;

    if (value && !range) {
        console.warn(`WARN: Invalid range provided for /persona-sync command: ${value}`);
        return '';
    }

    const quiet = !isFalseBoolean(args?.quiet);
    const nameFilter = typeof args?.from === 'string' ? args.from.trim() : '';
    const start = range ? range.start : 0;
    const end = range ? range.end : chat.length - 1;

    await syncUserNameToPersona({ start, end, quiet, nameFilter });

    return '';
}

/**
 * Returns all unique user message names in the current chat for enum autocomplete.
 * @returns {SlashCommandEnumValue[]}
 */
function userMessageNamesEnumProvider() {
    return chat
        .filter(mes => mes.is_user)
        .map(mes => mes.name)
        .filter(onlyUnique)
        .sort(sortIgnoreCaseAndAccents)
        .map(name => new SlashCommandEnumValue(name, null, enumTypes.name, enumIcons.persona));
}

function registerPersonaSlashCommands() {
    // Shared persona field definitions for persona CRUD commands
    const getPersonaFieldArgs = ({ requiredFields = [] } = {}) => [
        SlashCommandNamedArgument.fromProps({
            name: 'name',
            description: t`The name of the persona`,
            typeList: [ARGUMENT_TYPE.STRING],
            isRequired: requiredFields.includes('name'),
        }),
        SlashCommandNamedArgument.fromProps({
            name: 'description',
            description: t`The persona description (sent with messages for AI context)`,
            typeList: [ARGUMENT_TYPE.STRING],
            isRequired: requiredFields.includes('description'),
        }),
        SlashCommandNamedArgument.fromProps({
            name: 'title',
            description: t`A display title for the persona (not sent to the AI, display only)`,
            typeList: [ARGUMENT_TYPE.STRING],
            isRequired: requiredFields.includes('title'),
        }),
        SlashCommandNamedArgument.fromProps({
            name: 'avatar',
            description: t`Avatar image. Use "prompt" to open file picker, or provide a local ST file path or base64 data URL. Can also be the return value of /imagine.`,
            typeList: [ARGUMENT_TYPE.STRING],
            isRequired: requiredFields.includes('avatar'),
            enumList: [
                new SlashCommandEnumValue('prompt', 'Open file picker to select an image', enumTypes.enum, '📁'),
                new SlashCommandEnumValue('characters/...', 'Character avatars path (e.g., characters/Name.png)', enumTypes.enum, '📄', (input) => commonEnumMatchProviders.folderEnum(input, 'characters/'), () => 'characters/'),
                new SlashCommandEnumValue('backgrounds/...', 'Background image path', enumTypes.enum, '📄', (input) => commonEnumMatchProviders.folderEnum(input, 'backgrounds/'), () => 'backgrounds/'),
                new SlashCommandEnumValue('User Avatars/...', 'User avatar path', enumTypes.enum, '📄', (input) => commonEnumMatchProviders.folderEnum(input, 'User Avatars/'), () => 'User Avatars/'),
                new SlashCommandEnumValue('assets/...', 'Asset file path', enumTypes.enum, '📄', (input) => commonEnumMatchProviders.folderEnum(input, 'assets/'), () => 'assets/'),
                new SlashCommandEnumValue('user/images/...', 'User image path', enumTypes.enum, '📄', (input) => commonEnumMatchProviders.folderEnum(input, 'user/images/'), () => 'user/images/'),
            ],
        }),
        SlashCommandNamedArgument.fromProps({
            name: 'avatarPromptResize',
            description: t`Whether to show the avatar resize/crop dialog when uploading. Ignored if "Never resize avatars" is enabled in settings.`,
            typeList: [ARGUMENT_TYPE.BOOLEAN],
            defaultValue: 'true',
            enumProvider: commonEnumProviders.boolean('trueFalse'),
        }),
        SlashCommandNamedArgument.fromProps({
            name: 'descriptionPosition',
            description: t`Where to inject the persona description in the prompt`,
            typeList: [ARGUMENT_TYPE.STRING],
            enumList: [
                new SlashCommandEnumValue('inPrompt', t`In Prompt (default)`, enumTypes.enum),
                new SlashCommandEnumValue('topAN', t`Top of Author's Note`, enumTypes.enum),
                new SlashCommandEnumValue('bottomAN', t`Bottom of Author's Note`, enumTypes.enum),
                new SlashCommandEnumValue('atDepth', t`At a specific depth (uses descriptionDepth and descriptionRole)`, enumTypes.enum),
                new SlashCommandEnumValue('none', t`None (don't inject)`, enumTypes.enum),
            ],
        }),
        SlashCommandNamedArgument.fromProps({
            name: 'descriptionDepth',
            description: t`Depth for the persona description (when position is "atDepth")`,
            typeList: [ARGUMENT_TYPE.NUMBER],
        }),
        SlashCommandNamedArgument.fromProps({
            name: 'descriptionRole',
            description: t`Role for the persona description (when position is "atDepth")`,
            typeList: [ARGUMENT_TYPE.STRING],
            enumList: commonEnumProviders.messageRoles(),
        }),
        SlashCommandNamedArgument.fromProps({
            name: 'lorebook',
            description: t`The name of the lorebook/world info to attach to this persona`,
            typeList: [ARGUMENT_TYPE.STRING],
            enumProvider: commonEnumProviders.worlds,
        }),
    ];

    // Shared persona target argument (for commands that operate on an existing persona)
    const personaTargetArg = SlashCommandNamedArgument.fromProps({
        name: 'persona',
        description: t`Persona name or avatar key. If not provided, uses the currently active persona.`,
        typeList: [ARGUMENT_TYPE.STRING],
        enumProvider: commonEnumProviders.personas({ allowPersonaKey: true }),
    });

    // ========================
    // New CRUD commands
    // ========================

    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
        name: 'persona-create',
        callback: createPersonaCallback,
        returns: t`the avatar key (unique identifier) of the created persona`,
        namedArgumentList: [
            ...getPersonaFieldArgs({ requiredFields: ['name'] }),
            SlashCommandNamedArgument.fromProps({
                name: 'select',
                description: t`Whether to select/activate the persona after creation`,
                typeList: [ARGUMENT_TYPE.BOOLEAN],
                defaultValue: 'true',
                enumProvider: commonEnumProviders.boolean('trueFalse'),
            }),
        ],
        helpString: `
        <div>
            ${t`Creates a new persona with the specified attributes. Returns the avatar key of the created persona.`}
        </div>
        <div>
            <strong>${t`Required arguments:`}</strong>
            <ul>
                <li><code>name</code> – ${t`The persona's display name.`}</li>
            </ul>
        </div>
        <div>
            <strong>${t`Note on avatar:`}</strong>
            ${t`The <code>avatar</code> argument accepts <code>prompt</code> to open a file picker, a local ST file path, or a base64 data URL. Can also be the return value of <code>/imagine</code>. If not provided, a default avatar will be used.`}
        </div>
        <div>
            <strong>${t`Example:`}</strong>
            <ul>
                <li>
                    <pre><code>/persona-create name="Alice" description="A curious adventurer"</code></pre>
                </li>
                <li>
                    <pre><code>/persona-create name="Bob" avatar=prompt lorebook="detective_lore" select=false</code></pre>
                </li>
                <li>
                    <pre><code>/imagine portrait of an elf | /persona-create name="Elf" avatar="{{pipe}}"</code></pre>
                </li>
            </ul>
        </div>
        `,
    }));

    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
        name: 'persona-update',
        callback: updatePersonaCallback,
        returns: t`the avatar key of the updated persona`,
        namedArgumentList: [
            personaTargetArg,
            ...getPersonaFieldArgs(),
        ],
        helpString: `
        <div>
            ${t`Updates an existing persona's attributes. Only the provided fields are changed; others are left untouched.`}
        </div>
        <div>
            ${t`If no <code>persona</code> argument is provided, updates the currently active persona.`}
        </div>
        <div>
            <strong>${t`Example:`}</strong>
            <ul>
                <li>
                    <pre><code>/persona-update description="An updated description"</code></pre>
                    ${t`Updates the current persona's description.`}
                </li>
                <li>
                    <pre><code>/persona-update persona="Alice" name="Alice 2.0" descriptionPosition=atDepth descriptionDepth=3</code></pre>
                    ${t`Renames Alice and sets her description to inject at depth 3.`}
                </li>
                <li>
                    <pre><code>/imagine portrait | /persona-update avatar="{{pipe}}"</code></pre>
                    ${t`Generates an image and sets it as the current persona's avatar.`}
                </li>
            </ul>
        </div>
        `,
    }));

    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
        name: 'persona-get',
        aliases: ['persona-data'],
        callback: getPersonaDataCallback,
        returns: t`persona data as JSON or a specific field value`,
        namedArgumentList: [
            personaTargetArg,
            SlashCommandNamedArgument.fromProps({
                name: 'field',
                description: t`Specific field to retrieve. If not provided, returns the entire persona data as JSON.`,
                typeList: [ARGUMENT_TYPE.STRING],
                enumList: [
                    new SlashCommandEnumValue('name', t`Persona name`, enumTypes.enum, enumIcons.persona),
                    new SlashCommandEnumValue('description', t`Persona description`, enumTypes.enum, enumIcons.default),
                    new SlashCommandEnumValue('title', t`Display title`, enumTypes.enum, enumIcons.default),
                    new SlashCommandEnumValue('position', t`Description position (numeric)`, enumTypes.enum, enumIcons.default),
                    new SlashCommandEnumValue('depth', t`Description depth`, enumTypes.enum, enumIcons.default),
                    new SlashCommandEnumValue('role', t`Description role (numeric)`, enumTypes.enum, enumIcons.default),
                    new SlashCommandEnumValue('lorebook', t`Attached lorebook name`, enumTypes.enum, enumIcons.world),
                    new SlashCommandEnumValue('avatar', t`Avatar filename (unique key)`, enumTypes.enum, enumIcons.persona),
                    new SlashCommandEnumValue('default', t`Whether this is the default persona`, enumTypes.enum, enumIcons.default),
                    new SlashCommandEnumValue('connections', t`Character/group connections (array)`, enumTypes.enum, enumIcons.character),
                ],
            }),
            SlashCommandNamedArgument.fromProps({
                name: 'return',
                description: t`The way to return the result`,
                typeList: [ARGUMENT_TYPE.STRING],
                defaultValue: 'pipe',
                enumList: slashCommandReturnHelper.enumList({ allowPipe: true, allowObject: true, allowPopup: true, allowTextVersion: false }),
            }),
        ],
        helpString: `
        <div>
            ${t`Retrieves persona data. Can return all data as JSON or a specific field value.`}
        </div>
        <div>
            ${t`If no <code>persona</code> argument is provided, uses the currently active persona.`}
        </div>
        <div>
            <strong>${t`Example:`}</strong>
            <ul>
                <li>
                    <pre><code>/persona-get field=description | /echo</code></pre>
                    ${t`Outputs the current persona's description.`}
                </li>
                <li>
                    <pre><code>/persona-get persona="Alice" field=name</code></pre>
                    ${t`Returns Alice's persona name.`}
                </li>
                <li>
                    <pre><code>/persona-get return=object | /json-get key=avatar</code></pre>
                    ${t`Returns the current persona's full data as an object, then extracts the avatar key.`}
                </li>
            </ul>
        </div>
        `,
    }));

    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
        name: 'persona-delete',
        callback: deletePersonaCallback,
        returns: t`true if the persona was deleted, false otherwise`,
        namedArgumentList: [
            personaTargetArg,
            SlashCommandNamedArgument.fromProps({
                name: 'silent',
                description: t`Skip the confirmation popup`,
                typeList: [ARGUMENT_TYPE.BOOLEAN],
                defaultValue: 'false',
                enumProvider: commonEnumProviders.boolean('trueFalse'),
            }),
        ],
        helpString: `
        <div>
            ${t`Deletes a persona and its avatar from the system.`}
        </div>
        <div>
            ${t`If no <code>persona</code> argument is provided, deletes the currently active persona.`}
        </div>
        <div>
            <strong>⚠️ ${t`Warning:`}</strong> ${t`This action is irreversible. All data associated with the persona will be lost.`}
        </div>
        <div>
            <strong>${t`Example:`}</strong>
            <ul>
                <li>
                    <pre><code>/persona-delete</code></pre>
                    ${t`Deletes the current persona (shows confirmation popup).`}
                </li>
                <li>
                    <pre><code>/persona-delete persona="Bob" silent=true</code></pre>
                    ${t`Deletes Bob without confirmation.`}
                </li>
            </ul>
        </div>
        `,
    }));

    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
        name: 'persona-duplicate',
        callback: duplicatePersonaCallback,
        returns: t`the avatar key (unique identifier) of the duplicated persona`,
        namedArgumentList: [
            personaTargetArg,
            SlashCommandNamedArgument.fromProps({
                name: 'select',
                description: t`Whether to select/activate the duplicated persona after creation`,
                typeList: [ARGUMENT_TYPE.BOOLEAN],
                defaultValue: 'false',
                enumProvider: commonEnumProviders.boolean('trueFalse'),
            }),
        ],
        helpString: `
        <div>
            ${t`Duplicates a persona including all its data and avatar. Returns the avatar key of the new persona.`}
        </div>
        <div>
            ${t`Use <code>/persona-update</code> afterwards to rename or modify the duplicated persona's fields.`}
        </div>
        <div>
            <strong>${t`Example:`}</strong>
            <ul>
                <li>
                    <pre><code>/persona-duplicate</code></pre>
                    ${t`Duplicates the currently active persona.`}
                </li>
                <li>
                    <pre><code>/persona-duplicate persona="Alice" select=true</code></pre>
                    ${t`Duplicates Alice and selects the new persona.`}
                </li>
                <li>
                    <pre><code>/persona-duplicate | /persona-update persona="{{pipe}}" name="Clone"</code></pre>
                    ${t`Duplicates the current persona, then renames the clone.`}
                </li>
            </ul>
        </div>
        `,
    }));

    // ========================
    // Existing commands (enhanced help strings)
    // ========================

    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
        name: 'persona-lock',
        aliases: ['lock', 'bind'],
        callback: lockPersonaCallback,
        returns: t`The current lock state for the given type`,
        helpString: `
        <div>
            ${t`Locks/unlocks the current persona to a chat, character, or as the default. Returns the lock state if no value is provided.`}
        </div>
        <div>
            <strong>${t`Example:`}</strong>
            <ul>
                <li><pre><code>/persona-lock on</code></pre> ${t`Locks persona to this chat.`}</li>
                <li><pre><code>/persona-lock type=character on</code></pre> ${t`Locks persona to the current character.`}</li>
                <li><pre><code>/persona-lock type=default on</code></pre> ${t`Sets persona as the default for new chats.`}</li>
                <li><pre><code>/persona-lock</code></pre> ${t`Returns whether the persona is locked to this chat.`}</li>
            </ul>
        </div>
        `,
        namedArgumentList: [
            SlashCommandNamedArgument.fromProps({
                name: 'type',
                description: t`The type of the lock, where it should apply to`,
                typeList: [ARGUMENT_TYPE.STRING],
                defaultValue: 'chat',
                enumList: [
                    new SlashCommandEnumValue('chat', t`Lock the persona to the current chat.`),
                    new SlashCommandEnumValue('character', t`Lock this persona to the currently selected character. If the setting is enabled, multiple personas can be locked to the same character.`),
                    new SlashCommandEnumValue('default', t`Lock this persona as the default persona for all new chats.`),
                ],
            }),
        ],
        unnamedArgumentList: [
            SlashCommandArgument.fromProps({
                description: 'state',
                typeList: [ARGUMENT_TYPE.STRING],
                enumProvider: commonEnumProviders.boolean('onOffToggle'),
            }),
        ],
    }));

    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
        name: 'persona-set',
        callback: setNameCallback,
        aliases: ['persona', 'name'],
        namedArgumentList: [
            SlashCommandNamedArgument.fromProps({
                name: 'mode',
                description: t`The mode for persona selection`,
                typeList: [ARGUMENT_TYPE.STRING],
                defaultValue: 'all',
                enumList: [
                    new SlashCommandEnumValue('lookup', t`Search for an existing persona only`),
                    new SlashCommandEnumValue('temp', t`Set a temporary name only (no persona lookup)`),
                    new SlashCommandEnumValue('all', t`Try persona lookup first, fall back to temporary name`),
                ],
            }),
        ],
        unnamedArgumentList: [
            SlashCommandArgument.fromProps({
                description: 'persona name',
                typeList: [ARGUMENT_TYPE.STRING],
                isRequired: true,
                enumProvider: commonEnumProviders.personas({ allowPersonaKey: true }),
            }),
        ],
        helpString: `
        <div>
            ${t`Selects an existing persona by name or avatar key, or sets a temporary user name.`}
        </div>
        <div>
            ${t`If a matching persona exists, it will be selected with its name and avatar. Otherwise (in "all" or "temp" mode), only the display name is changed temporarily.`}
        </div>
        <div>
            <strong>${t`Example:`}</strong>
            <ul>
                <li><pre><code>/persona-set Alice</code></pre> ${t`Selects persona "Alice", or sets name to "Alice" if not found.`}</li>
                <li><pre><code>/persona-set mode=lookup Alice</code></pre> ${t`Only selects if persona "Alice" exists.`}</li>
            </ul>
        </div>
        `,
    }));

    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
        name: 'persona-sync',
        aliases: ['sync'],
        callback: syncCallback,
        namedArgumentList: [
            SlashCommandNamedArgument.fromProps({
                name: 'from',
                description: t`only sync messages from a certain persona name`,
                typeList: [ARGUMENT_TYPE.STRING],
                enumProvider: userMessageNamesEnumProvider,
            }),
            SlashCommandNamedArgument.fromProps({
                name: 'quiet',
                description: t`suppress the confirmation popup`,
                typeList: [ARGUMENT_TYPE.BOOLEAN],
                enumList: commonEnumProviders.boolean('trueFalse')(),
                defaultValue: 'true',
            }),
        ],
        unnamedArgumentList: [
            SlashCommandArgument.fromProps({
                description: t`message index (starts with 0) or range, syncs all user messages if not provided`,
                typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
                defaultValue: '0-{{lastMessageId}}',
            }),
        ],
        helpString: `
        <div>
            ${t`Syncs the user persona (name and avatar) in user-attributed messages in the current chat.`}
        </div>
        <div>
            ${t`If <code>from</code> is set, only messages with that specific persona name will be synced. Useful when multiple personas have been used in the same chat.`}
        </div>
        <div>
            ${t`If <code>quiet</code> is set to <code>false</code>, a confirmation popup will be shown before syncing.`}
        </div>
        <div>
            <strong>${t`Examples:`}</strong>
            <ul>
                <li><pre><code>/persona-sync</code></pre> ${t`- Sync all user messages`}</li>
                <li><pre><code>/persona-sync 5</code></pre> ${t`- Sync only message 5`}</li>
                <li><pre><code>/persona-sync 0-10</code></pre> ${t`- Sync messages 0 through 10`}</li>
                <li><pre><code>/persona-sync from=OldPersona 0-20</code></pre> ${t`- Sync only messages with name "OldPersona" in range 0-20`}</li>
                <li><pre><code>/persona-sync quiet=false</code></pre> ${t`- Sync all with confirmation popup`}</li>
                <li><pre><code>/persona-sync from=TempName quiet=false 5-15</code></pre> ${t`- Sync messages with name "TempName" in range 5-15 with confirmation`}</li>
            </ul>
        </div>
    `,
    }));
}

/**
 * Initializes the persona management and all its functionality.
 * This is called during the initialization of the page.
 */
export async function initPersonas() {
    await migrateNonPersonaUser();
    registerPersonaSlashCommands();
    $('#persona_delete_button').on('click', deleteUserAvatar);
    $('#lock_persona_default').on('click', () => togglePersonaLock('default'));
    $('#lock_user_name').on('click', () => togglePersonaLock('chat'));
    $('#lock_persona_to_char').on('click', () => togglePersonaLock('character'));
    $('#create_dummy_persona').on('click', createDummyPersona);
    $('#persona_description').on('input', onPersonaDescriptionInput);
    $('#persona_description_position').on('input', onPersonaDescriptionPositionInput);
    $('#persona_depth_value').on('input', onPersonaDescriptionDepthValueInput);
    $('#persona_depth_role').on('input', onPersonaDescriptionDepthRoleInput);
    $('#persona_lore_button').on('click', onPersonaLoreButtonClick);
    addLongPressEvent('#persona_lore_button', function () {
        onPersonaLoreButtonClick({ shiftKey: true, altKey: false });
    });
    $('#persona-management-dropdown').on('change', async function () {
        const target = $(this).find(':selected').attr('id');
        $(this).prop('selectedIndex', 0);
        switch (target) {
            case 'persona_lorebook_link':
                await onPersonaLoreButtonClick({ shiftKey: true, altKey: false });
                break;
        }
    });
    $('#personas_backup').on('click', onBackupPersonas);
    $('#personas_restore').on('click', () => $('#personas_restore_input').trigger('click'));
    $('#personas_restore_input').on('change', onPersonasRestoreInput);
    $('#persona_sort_order').val(power_user.persona_sort_order).on('input', function () {
        const value = String($(this).val());
        // Save sort order, but do not save search sorting, as this is a temporary sorting option
        if (value !== 'search') power_user.persona_sort_order = value;
        getUserAvatars(true, user_avatar);
        saveSettingsDebounced();
    });
    $('#persona_grid_toggle').on('click', () => {
        const state = accountStorage.getItem(GRID_STORAGE_KEY) === 'true';
        accountStorage.setItem(GRID_STORAGE_KEY, String(!state));
        switchPersonaGridView();
    });

    const debouncedPersonaSearch = debounce((searchQuery) => {
        personasFilter.setFilterData(FILTER_TYPES.PERSONA_SEARCH, searchQuery);
    });

    $('#persona_search_bar').on('input', function () {
        const searchQuery = String($(this).val());
        debouncedPersonaSearch(searchQuery);
    });

    $('#sync_name_button').on('click', async () => await syncUserNameToPersona());
    $('#avatar_upload_file').on('change', changeUserAvatar);

    $(document).on('click', '#user_avatar_block .avatar-container', async function () {
        const imgfile = $(this).attr('data-avatar-id');
        await setUserAvatar(imgfile);
    });

    $('#persona_rename_button').on('click', () => renamePersona(user_avatar));

    $(document).on('click', '#user_avatar_block .avatar_upload', function () {
        $('#avatar_upload_overwrite').val('');
        $('#avatar_upload_file').trigger('click');
    });

    $('#persona_duplicate_button').on('click', () => duplicatePersona(user_avatar));

    $('#persona_set_image_button').on('click', function () {
        if (!user_avatar) {
            console.log('no imgfile');
            return;
        }

        $('#avatar_upload_overwrite').val(user_avatar);
        $('#avatar_upload_file').trigger('click');
    });

    $('#char_connections_button').on('click', showCharConnections);

    eventSource.on(event_types.CHARACTER_MANAGEMENT_DROPDOWN, (target) => {
        if (target === 'convert_to_persona') {
            convertCharacterToPersona();
        }
    });
    eventSource.on(event_types.CHAT_CHANGED, updatePersonaUIStates);
    eventSource.on(event_types.CHAT_CHANGED, loadPersonaForCurrentChat);
    switchPersonaGridView();
}
