Disappear

Delete all your messages in specified Discord DMs, groups, and servers.

// ==UserScript==
// @name         Disappear
// @namespace    https://github.com/vil/disappear
// @version      1.0
// @description  Delete all your messages in specified Discord DMs, groups, and servers.
// @author       Vili (https://vili.dev)
// @match        https://discord.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // Helper to parse header string to Headers object
    function createHeadersFromString(headerStr) {
        const headers = new Headers();
        if (headerStr) {
            const headerPairs = headerStr.split('\r\n');
            headerPairs.forEach(headerPair => {
                const P = headerPair.indexOf(':');
                if (P > 0) {
                    const key = headerPair.substring(0, P).trim();
                    const value = headerPair.substring(P + 1).trim();
                    if (key && value) { // Ensure key and value are not empty
                        try {
                            headers.append(key, value);
                        } catch (e) {
                            console.warn(`[Disappear] Could not append header: ${key}: ${value}`, e);
                            // Some headers might be problematic for the Headers object if they are not valid
                        }
                    }
                }
            });
        }
        return headers;
    }

    // --- Configuration ---
    const SCRIPT_PREFIX = 'disappear'; // Used for CSS classes and local storage
    const API_BASE_URL = 'https://discord.com/api/v9';
    const MESSAGE_FETCH_LIMIT = 100; // Max messages to fetch per request (Discord limit)
    let MIN_DELAY_MS = 1000; // Minimum delay between delete operations
    let MAX_DELAY_MS = 3000; // Maximum delay
    const MAX_MESSAGE_DELETE_ATTEMPTS = 3; // Max attempts to delete a single message
    const MAX_CONSECUTIVE_EMPTY_USER_MESSAGE_FETCHES = 3; // Max consecutive full fetches with no user messages before stopping channel scan

    // --- State ---
    let authToken = '';
    let currentUserId = null;
    let isDeleting = false; // To control the deletion loop
    let isExporting = false; // To control the export loop

    // --- UI Elements ---
    let controlButton;
    let modalContainer;
    let statusDiv;

    // --- Helper Function for Control Button State ---
    function updateControlButtonIndicator(isWorking) {
        if (!controlButton) return;
        const workingClassName = `${SCRIPT_PREFIX}-working-indicator`;
        if (isWorking) {
            controlButton.innerHTML = '🗑️ Disappear (Working...)';
            controlButton.classList.add(workingClassName);
        } else {
            controlButton.innerHTML = '🗑️ Disappear';
            controlButton.classList.remove(workingClassName);
        }
    }

    // --- API Interaction ---
    function discordApiRequest(method, endpoint, data) {
        return new Promise((resolve, reject) => {
            if (!authToken) {
                updateStatus('<span style="color: red;">Auth Token is not set!</span>', true);
                return reject({ message: 'Auth Token is not set!', status: 0, responseText: null });
            }
            GM_xmlhttpRequest({
                method: method,
                url: `${API_BASE_URL}${endpoint}`,
                headers: {
                    "Authorization": authToken,
                    "Content-Type": "application/json"
                },
                data: data ? JSON.stringify(data) : null,
                onload: function(response) {
                    const responseHeaders = createHeadersFromString(response.responseHeaders);
                    if (response.status >= 200 && response.status < 300) {
                        try {
                            resolve({ data: JSON.parse(response.responseText), headers: responseHeaders });
                        } catch (e) { // Handle cases where response might not be JSON (e.g., 204 No Content for DELETE)
                            resolve({ data: null, headers: responseHeaders });
                        }
                    } else if (response.status === 401) {
                        updateStatus('<span style="color: red;">Invalid Auth Token! Please check and re-enter.</span>', true);
                        reject({
                            message: `API Error ${response.status}: Unauthorized. Invalid token?`,
                            status: response.status,
                            responseText: response.responseText,
                            headers: responseHeaders
                        });
                    } else if (response.status === 429) { // Rate limit
                        updateStatus('<span style="color: orange;">Rate limited by Discord.</span>', true);
                        reject({
                            message: 'Rate limited by Discord API.',
                            status: response.status,
                            headers: responseHeaders,
                            responseText: response.responseText
                        });
                    }
                    else { // Other errors (e.g. 403, 404, 500)
                        updateStatus(`<span style="color: red;">API Error ${response.status}. Check console.</span>`, true);
                        console.error('[Disappear] API Error Response:', response);
                        reject({
                            message: `API Error ${response.status}: ${response.statusText}`,
                            status: response.status,
                            responseText: response.responseText,
                            headers: responseHeaders // Also pass headers for other errors if available
                        });
                    }
                },
                onerror: function(error) {
                    updateStatus('<span style="color: red;">Network error. Check console.</span>', true);
                    console.error('[Disappear] Network Error:', error);
                    reject({
                        message: 'Network error during API request.',
                        status: 0, // Or some other indicator for network error
                        responseText: null,
                        networkError: true,
                        headers: new Headers() // Provide empty headers for network errors
                    });
                }
            });
        });
    }

    async function fetchAuthenticatedUser() {
        if (currentUserId) return currentUserId;
        try {
            updateStatus('Fetching user ID...');
            const { data } = await discordApiRequest('GET', '/users/@me');
            if (data && data.id) {
                currentUserId = data.id;
                updateStatus('User ID fetched successfully.');
                console.log('[Disappear] Current User ID:', currentUserId);
                return currentUserId;
            } else {
                throw new Error('Could not retrieve user ID.');
            }
        } catch (error) {
            console.error('[Disappear] Error fetching user ID:', error);
            updateStatus('<span style="color: red;">Failed to fetch User ID. Token might be invalid.</span>', true);
            throw error;
        }
    }


    async function fetchChannels() {
        updateStatus('Fetching channels...');
        console.log('[Disappear]', 'Fetching channels...');
        try {
            // Fetch DMs (includes user DMs and group DMs)
            const { data: dmChannels } = await discordApiRequest('GET', '/users/@me/channels');
            const channels = dmChannels.map(ch => ({
                id: ch.id,
                name: ch.recipients && ch.recipients.length > 0 ? ch.recipients.map(r => r.username).join(', ') : (ch.name || 'Unnamed Group DM'),
                type: ch.type === 1 ? 'DM' : (ch.type === 3 ? 'Group DM' : 'Unknown DM Type') // 1: DM, 3: Group DM
            }));

            // Fetch Guilds (servers)
            const { data: guilds } = await discordApiRequest('GET', '/users/@me/guilds');
            for (const guild of guilds) {
                // For each guild, fetch its channels
                // Note: This can be a lot of requests if the user is in many servers.
                // @TODO Consider adding an option to only fetch channels for selected servers later.
                try {
                    const { data: guildChannels } = await discordApiRequest('GET', `/guilds/${guild.id}/channels`);
                    guildChannels.forEach(gc => {
                        // We are primarily interested in text channels where messages can be sent
                        if (gc.type === 0 || gc.type === 2 || gc.type === 5 || gc.type === 10 || gc.type === 11 || gc.type === 12) { // Text, Voice (text), Announcement, Thread types
                             channels.push({
                                id: gc.id,
                                name: `${gc.name} (${guild.name})`,
                                type: 'Server Channel'
                            });
                        }
                    });
                } catch (guildChannelError) {
                    console.warn(`[Disappear] Could not fetch channels for guild ${guild.name} (ID: ${guild.id}):`, guildChannelError);
                }
            }
            updateStatus('Channels fetched.');
            console.log('[Disappear] Fetched channels:', channels);
            return channels;
        } catch (error) {
            console.error('[Disappear] Error fetching channels:', error);
            updateStatus('<span style="color: red;">Failed to fetch channels.</span>', true);
            throw error;
        }
    }

    async function fetchMessages(channelId, authorId, beforeMessageId = null) {
        const logMessage = beforeMessageId ? 
            `Scanning for messages (before ${beforeMessageId}) in channel ${channelId}...` :
            `Scanning for initial messages in channel ${channelId}...`;
        updateStatus(logMessage, true); // Append this specific log
        console.log('[Disappear]', `Fetching messages for channel ${channelId}, author ${authorId}, before ${beforeMessageId || 'latest'}`);
        let endpoint = `/channels/${channelId}/messages?limit=${MESSAGE_FETCH_LIMIT}`;
        if (beforeMessageId) {
            endpoint += `&before=${beforeMessageId}`;
        }

        try {
            const { data: allMessages, headers } = await discordApiRequest('GET', endpoint);
            
            if (!allMessages || allMessages.length === 0) {
                // No messages returned at all from the API for this segment
                return { userMessages: [], oldestMessageIdInBatch: null, hasMore: false, headers, skipped: false };
            }

            // Filter messages by the current user
            const userMessages = allMessages.filter(msg => msg.author.id === authorId);
            const oldestMessageIdInBatch = allMessages[allMessages.length - 1].id;
            const hasMoreMessagesInChannel = allMessages.length === MESSAGE_FETCH_LIMIT;

            console.log('[Disappear]', `Fetched ${allMessages.length} raw messages, ${userMessages.length} by user ${authorId}. Oldest in batch: ${oldestMessageIdInBatch}`);
            return { userMessages, oldestMessageIdInBatch, hasMore: hasMoreMessagesInChannel, headers, skipped: false };
        } catch (error) {
            if (error.status === 429) { // Rate limit
                await handleRateLimit(new Headers(Object.entries(error.headers).map(([k,v]) => [k, Array.isArray(v) ? v[0] : v]))); // GM_xmlhttpRequest headers are different
                 // After handling rate limit, throw a specific error or return a value indicating a retry is needed for the fetch itself
                 // This allows the calling function (processChannel) to decide to retry fetching this same segment.
                 throw { ...error, needsRetryFetch: true }; 
            }
            console.error('[Disappear] Error fetching messages:', error);
            updateStatus(`<span style="color: red;">Failed to fetch messages for channel ${channelId}.</span>`, true);
            if (error.message && (error.message.includes('403') || error.message.includes('Missing Access'))) {
                 console.warn(`[Disappear] Missing access to channel ${channelId}. Skipping.`);
                 return { userMessages: [], oldestMessageIdInBatch: null, hasMore: false, headers: null, skipped: true };
            }
            throw error; // Re-throw other errors
        }
    }

    async function deleteMessage(channelId, messageId) {
        console.log('[Disappear]', `Attempting delete for message ${messageId} in channel ${channelId}...`);
        try {
            const { headers } = await discordApiRequest('DELETE', `/channels/${channelId}/messages/${messageId}`);
            // Handle rate limits after successful delete too, as the limit might be hit on the NEXT request
            if (headers && headers.get('x-ratelimit-remaining') === '0') {
                 const delay = (parseFloat(headers.get('x-ratelimit-reset-after')) || (MIN_DELAY_MS/1000) + Math.random() * (MAX_DELAY_MS - MIN_DELAY_MS)/1000) * 1000;
                 console.warn(`[Disappear] Approaching rate limit (on success), waiting ${delay.toFixed(0)}ms`);
                 await new Promise(resolve => setTimeout(resolve, delay));
            } else {
                // Normal delay
                const delay = MIN_DELAY_MS + Math.random() * (MAX_DELAY_MS - MIN_DELAY_MS);
                await new Promise(resolve => setTimeout(resolve, delay));
            }
            return { success: true, headers };
        } catch (error) {
            // Check for system message error (Discord code 50021)
            if (error.status === 403 && error.responseText) {
                try {
                    const errorData = JSON.parse(error.responseText);
                    if (errorData.code === 50021 && errorData.message === "Cannot execute action on a system message") {
                        console.warn(`[Disappear] System message (${messageId}). Code 50021. Skipping.`);
                        updateStatus(`<span style="color: #ccaa00;">System msg ${messageId}. Skipping.</span>`, true);
                        return { success: false, systemMessage: true };
                    }
                } catch (parseError) {
                    console.warn('[Disappear] Could not parse error.responseText for 403 error:', parseError, error.responseText);
                }
            }

            // Handle "Unknown Message" error (Discord code 10008)
            if (error.status === 404 && error.responseText) {
                try {
                    const errorData = JSON.parse(error.responseText);
                    if (errorData.code === 10008) {
                        console.warn(`[Disappear] Unknown Message (${messageId}). Code 10008. Already deleted or inaccessible. Skipping.`);
                        updateStatus(`<span style="color: #ccaa00;">Msg ${messageId} not found (10008). Already deleted or inaccessible. Skipping.</span>`, true);
                        return { success: false, unknownMessage: true }; // Special flag for this case
                    }
                } catch (parseError) {
                    console.warn('[Disappear] Could not parse error.responseText for 404 error:', parseError, error.responseText);
                }
            }

            if (error.status === 429) { // Rate limit
                 await handleRateLimit(new Headers(Object.entries(error.headers || {}).map(([k,v]) => [k, Array.isArray(v) ? v[0] : v])));
                 return { success: false, rateLimited: true }; // Indicate retry is needed by processChannel
            }

            // For other errors, log and re-throw to be handled by processChannel's retry logic
            console.error(`[Disappear] Error deleting message ${messageId}:`, error.status, error.message, error.responseText);
            // Status update for this will be in processChannel's catch block
            throw error; // This error will be caught by the `catch (deleteError)` in `processChannel`
        }
    }

    async function fetchAllUserMessagesForExport(channelId, authorId, channelName) {
        if (!isExporting) {
            updateStatus(`Export stopped for channel ${channelId}.`);
            return { messages: [], skipped: false, error: false };
        }
        // Set initial status for the channel export, subsequent logs from fetchMessages will append.
        updateStatus(`Preparing to export messages from channel ${channelName} (${channelId})...`, false);
        console.log('[Disappear]', `Fetching all messages for export from channel ${channelId} (${channelName}) for author ${authorId}`);

        let allUserMessagesInChannel = [];
        let lastFetchedMessageId = null;
        let consecutiveFetchFailures = 0;
        let consecutiveEmptyUserMessageFetches = 0;

        while (isExporting) {
            try {
                const {
                    userMessages,
                    oldestMessageIdInBatch,
                    hasMore,
                    headers,
                    skipped
                } = await fetchMessages(channelId, authorId, lastFetchedMessageId);

                if (skipped) {
                    updateStatus(`Skipped channel ${channelName} (${channelId}) for export due to missing access.`);
                    return { messages: [], skipped: true, error: false };
                }
                consecutiveFetchFailures = 0;

                if (userMessages && userMessages.length > 0) {
                    consecutiveEmptyUserMessageFetches = 0;
                    userMessages.forEach(msg => {
                        allUserMessagesInChannel.push({
                            message_id: msg.id,
                            timestamp: msg.timestamp,
                            edited_timestamp: msg.edited_timestamp || null,
                            content: msg.content,
                            attachments: msg.attachments,
                            embeds: msg.embeds,
                            author_id: msg.author.id,
                            author_username: msg.author.username,
                            channel_id: channelId,
                            channel_name: channelName
                        });
                    });
                    updateStatus(`Fetched ${userMessages.length} more messages from ${channelName}. Total for channel: ${allUserMessagesInChannel.length}. Scanning older...`, true);
                } else {
                    if (hasMore) {
                        consecutiveEmptyUserMessageFetches++;
                        updateStatus(`No user messages in this segment of ${channelName} (Scan ${consecutiveEmptyUserMessageFetches}/${MAX_CONSECUTIVE_EMPTY_USER_MESSAGE_FETCHES}). Scanning older...`, true);
                        if (consecutiveEmptyUserMessageFetches >= MAX_CONSECUTIVE_EMPTY_USER_MESSAGE_FETCHES) {
                            updateStatus(`No user messages found after ${MAX_CONSECUTIVE_EMPTY_USER_MESSAGE_FETCHES} scans in ${channelName}. Assuming all user messages fetched.`, true);
                            break;
                        }
                    }
                }

                if (!hasMore) {
                    updateStatus(`All user messages likely fetched from ${channelName}. Total: ${allUserMessagesInChannel.length}.`, true);
                    break;
                }

                if (!oldestMessageIdInBatch) {
                    updateStatus(`No oldest message ID to paginate with in ${channelName} for export. Stopping channel scan.`, true);
                    break;
                }
                lastFetchedMessageId = oldestMessageIdInBatch;

                const delay = (MIN_DELAY_MS / 2 + Math.random() * (MAX_DELAY_MS - MIN_DELAY_MS) / 2);
                await new Promise(resolve => setTimeout(resolve, Math.max(250, delay / 2))); // Shorter delay for fetching

            } catch (fetchError) {
                consecutiveFetchFailures++;
                console.error(`[Disappear] Error during message export fetch for ${channelName} (Attempt ${consecutiveFetchFailures}):`, fetchError);

                if (fetchError.needsRetryFetch) {
                    updateStatus(`<span style="color: orange;">Rate limited fetching messages for ${channelName} (export). Retrying...</span>`, true);
                    if (!fetchError.status || fetchError.status !== 429) {
                        await new Promise(resolve => setTimeout(resolve, MIN_DELAY_MS));
                    }
                    continue;
                }

                if (fetchError.status === 429) {
                    await handleRateLimit(new Headers(Object.entries(fetchError.headers || {}).map(([k,v]) => [k, Array.isArray(v) ? v[0] : v])));
                } else if (consecutiveFetchFailures >= 3) {
                    updateStatus(`<span style="color: red;">Too many errors exporting from ${channelName}. Skipping channel.</span>`, true);
                    return { messages: allUserMessagesInChannel, skipped: true, error: true };
                } else {
                    updateStatus(`<span style="color: orange;">Error exporting from ${channelName}. Retrying... (Attempt ${consecutiveFetchFailures})</span>`, true);
                    await new Promise(resolve => setTimeout(resolve, (MAX_DELAY_MS / 2) * consecutiveFetchFailures));
                }
            }
        }
        if (!isExporting) {
            updateStatus(`Export stopped by user during scan of ${channelName}.`, true);
        }
        return { messages: allUserMessagesInChannel, skipped: false, error: false };
    }

    async function processChannel(channelId, settings) {
        if (!isDeleting) {
            updateStatus(`Deletion stopped for channel ${channelId}.`);
            return;
        }
        // Set initial status for the channel, subsequent logs from fetchMessages will append.
        updateStatus(`Processing channel ${channelId}... (This may take a while, fetching message segments)`, false);
        console.log('[Disappear]', `Processing channel ${channelId}...`);

        if (!currentUserId) {
            updateStatus('<span style="color: red;">User ID not available. Cannot process channel.</span>', true);
            return;
        }

        let lastProcessedMessageId = null; // This will be the ID of the oldest message from the *previous* successful fetch operation
        let messagesDeletedInChannel = 0;
        let consecutiveFetchFailures = 0;
        let consecutiveEmptyUserMessageFetches = 0; // New counter

        while (isDeleting) {
            try {
                const { userMessages, oldestMessageIdInBatch, hasMore, headers, skipped } = await fetchMessages(channelId, currentUserId, lastProcessedMessageId);

                if (skipped) {
                    updateStatus(`Skipped channel ${channelId} due to missing access.`);
                    return; // Stop processing this channel
                }
                consecutiveFetchFailures = 0; // Reset on successful fetch (even if no user messages)

                if (userMessages && userMessages.length > 0) {
                    consecutiveEmptyUserMessageFetches = 0; // Reset if we found user messages
                    for (const message of userMessages) {
                        if (!isDeleting) {
                            updateStatus('Deletion process stopped globally.');
                            return;
                        }

                        // Proactively skip non-standard message types, even if authored by the user
                        const userDeletableTypes = [0, 19, 20, 21, 23]; // 0:Default, 19:Reply, 20:ChatInputCommand, 21:ThreadStarterMessage, 23:ContextMenuCommand
                        if (!userDeletableTypes.includes(message.type)) {
                            console.log(`[Disappear] Skipping message ${message.id} (type ${message.type}) in channel ${channelId}: Authored by user, but not a standard deletable message type.`);
                            updateStatus(`Skipping your msg ${message.id} (type ${message.type}) as it's not a standard deletable type.`, true);
                            continue; // Move to the next message
                        }

                        let attempts = 0;
                        let processedSuccessfully = false; // True if deleted or intentionally skipped (e.g. system message)

                        while (attempts < MAX_MESSAGE_DELETE_ATTEMPTS && !processedSuccessfully && isDeleting) {
                            try {
                                updateStatus(`Deleting msg ${message.id} (Ch:${channelId}) (Attempt ${attempts + 1}/${MAX_MESSAGE_DELETE_ATTEMPTS}, Total Ch Del: ${messagesDeletedInChannel})`);
                                const deleteResult = await deleteMessage(channelId, message.id);

                                if (deleteResult.success) {
                                    messagesDeletedInChannel++;
                                    updateStatus(`Deleted msg ${message.id} (Total Ch Del: ${messagesDeletedInChannel}). Waiting...`, true);
                                    processedSuccessfully = true;
                                } else if (deleteResult.systemMessage) {
                                    // Status already updated by deleteMessage
                                    processedSuccessfully = true; // Mark as "handled" to exit the retry loop
                                } else if (deleteResult.unknownMessage) { // Handle Unknown Message (10008)
                                    // Status already updated by deleteMessage
                                    processedSuccessfully = true; // Mark as "handled" to exit the retry loop
                                } else if (deleteResult.rateLimited) {
                                    updateStatus(`Rate limited on msg ${message.id}. Retrying after pause...`, true);
                                    // deleteMessage already paused. Loop will retry this message.
                                    // Do not increment attempts here.
                                } else {
                                    // This case should ideally not be reached if deleteMessage covers all returns or throws.
                                    attempts++;
                                    console.warn(`[Disappear] Unknown outcome from deleteMessage for ${message.id}. Attempt ${attempts}.`);
                                    if (attempts >= MAX_MESSAGE_DELETE_ATTEMPTS) {
                                        updateStatus(`<span style="color: red;">Unknown issue, msg ${message.id} skipped after ${attempts} attempts.</span>`, true);
                                        processedSuccessfully = true; // Give up
                                    } else if (isDeleting) {
                                        updateStatus(`<span style="color: orange;">Unknown issue with msg ${message.id}. Retrying (Att ${attempts + 1})...</span>`, true);
                                        await new Promise(resolve => setTimeout(resolve, MIN_DELAY_MS)); // Small delay
                                    }
                                }
                            } catch (deleteError) { // Catches errors THROWN by deleteMessage
                                attempts++;
                                const errStatus = deleteError.status || 'N/A';
                                const errMsg = deleteError.message || 'Unknown error';
                                console.warn(`[Disappear] Error deleting message ${message.id} (Attempt ${attempts}/${MAX_MESSAGE_DELETE_ATTEMPTS}): Status ${errStatus}, ${errMsg}`, deleteError.responseText || '');
                                
                                if (attempts >= MAX_MESSAGE_DELETE_ATTEMPTS) {
                                    updateStatus(`<span style="color: red;">Failed to delete msg ${message.id} after ${attempts} attempts (Status: ${errStatus}). Skipping.</span>`, true);
                                    processedSuccessfully = true; // Give up on this message
                                } else if (isDeleting) {
                                    updateStatus(`<span style="color: orange;">Error on msg ${message.id} (Status: ${errStatus}). Retrying (Att ${attempts + 1}/${MAX_MESSAGE_DELETE_ATTEMPTS})...</span>`, true);
                                    // Escalating delay for retries on the same message
                                    await new Promise(resolve => setTimeout(resolve, (MIN_DELAY_MS / 2 + Math.random() * (MAX_DELAY_MS - MIN_DELAY_MS)/2) * attempts));
                                }
                            }
                        } // End of while-retry loop for a single message

                        if (!processedSuccessfully && isDeleting) {
                             // This primarily catches cases where loop exited due to !isDeleting during retries
                             console.log(`[Disappear] Message ${message.id} in channel ${channelId} was not successfully processed.`);
                        } else if (!processedSuccessfully && !isDeleting) {
                            updateStatus(`Stopped while trying to process message ${message.id}.`, true);
                        }
                    } // End of for-loop for messages in current batch
                } else { // No user messages in this batch
                    if (hasMore) { // Batch was full, but no user messages
                        consecutiveEmptyUserMessageFetches++;
                        updateStatus(`No user messages in this segment of ${channelId} (Attempt ${consecutiveEmptyUserMessageFetches}/${MAX_CONSECUTIVE_EMPTY_USER_MESSAGE_FETCHES}). Scanning older...`, true);
                        if (consecutiveEmptyUserMessageFetches >= MAX_CONSECUTIVE_EMPTY_USER_MESSAGE_FETCHES) {
                            updateStatus(`No user messages found after ${MAX_CONSECUTIVE_EMPTY_USER_MESSAGE_FETCHES} consecutive empty scans in ${channelId}. Assuming all user messages are processed.`, true);
                            console.log(`[Disappear] Stopping channel ${channelId} after ${MAX_CONSECUTIVE_EMPTY_USER_MESSAGE_FETCHES} consecutive empty user message fetches.`);
                            break; // Exit while loop for this channel
                        }
                    } else {
                        // If !hasMore and userMessages.length is 0, the condition below will catch it.
                        // No need to increment consecutiveEmptyUserMessageFetches here.
                    }
                }

                if (!hasMore) { // No more messages in the channel according to API (current batch was not full)
                    updateStatus(`All your messages likely deleted in channel ${channelId}. Total for channel: ${messagesDeletedInChannel}`);
                    console.log(`[Disappear] No more messages (hasMore=false) or oldestMessageIdInBatch is null for channel ${channelId}.`);
                    break; // Exit while loop for this channel
                }

                if (!oldestMessageIdInBatch) {
                    // This case should ideally be covered by !hasMore if API returns empty and it's the end.
                    // If oldestMessageIdInBatch is null but hasMore was somehow true, it's an issue.
                    updateStatus(`No oldest message ID to paginate with in ${channelId}, but API indicated more. Stopping channel to be safe.`);
                    console.warn(`[Disappear] Inconsistent state: hasMore is true but no oldestMessageIdInBatch for channel ${channelId}.`);
                    break;
                }

                // Prepare for the next fetch iteration
                lastProcessedMessageId = oldestMessageIdInBatch;

            } catch (fetchOrProcessError) {
                consecutiveFetchFailures++;
                console.error(`[Disappear] Error during fetch/process for channel ${channelId} (Attempt ${consecutiveFetchFailures}):`, fetchOrProcessError);

                if (fetchOrProcessError.needsRetryFetch) { // Specific error from fetchMessages indicating a rate limit retry for the fetch itself
                    updateStatus(`<span style="color: orange;">Rate limited on fetching messages for ${channelId}. Automatic retry after pause...</span>`, true);
                    // fetchMessages already paused due to GM_xmlhttpRequest 429, or its internal handleRateLimit on other errors.
                    // We just continue the loop, and it will try fetching the same segment (using same lastProcessedMessageId).
                    // No need for additional manual pause here as handleRateLimit in fetchMessages should have done it.
                    // If fetchMessages didn't pause (e.g. error was not 429 but some other retryable), ensure a small delay.
                     if (!fetchOrProcessError.status || fetchOrProcessError.status !== 429) {
                         await new Promise(resolve => setTimeout(resolve, MIN_DELAY_MS)); // Small safety delay if not a 429
                     }
                    continue; // Retry fetching this segment
                }

                if (fetchOrProcessError.status === 429) { // General rate limit (e.g. from delete that propagated)
                    await handleRateLimit(new Headers(Object.entries(fetchOrProcessError.headers || {}).map(([k,v]) => [k, Array.isArray(v) ? v[0] : v])));
                } else if (consecutiveFetchFailures >= 3) {
                    updateStatus(`<span style="color: red;">Too many errors for channel ${channelId}. Skipping this channel.</span>`, true);
                    console.error(`[Disappear] Too many consecutive errors for channel ${channelId}. Skipping this channel.`);
                    break; // Stop processing this channel
                } else {
                    updateStatus(`<span style="color: orange;">Error in channel ${channelId}. Retrying after a delay... (Attempt ${consecutiveFetchFailures})</span>`, true);
                    await new Promise(resolve => setTimeout(resolve, MAX_DELAY_MS * consecutiveFetchFailures));
                }
            }
        }
        if (isDeleting) { // Only log as completed if deletion wasn't stopped globally
            console.log(`[Disappear] Finished processing channel ${channelId}. Deleted ${messagesDeletedInChannel} messages.`);
            updateStatus(`Finished scan for channel ${channelId}. Total messages deleted in this channel: ${messagesDeletedInChannel}.`, true);
        }
    }

    function handleRateLimit(responseHeaders) { // Changed to accept responseHeaders object
        let retryAfter = 1; // Default retry after 1 second
        if (responseHeaders) {
            // Try to get from 'retry-after' (standard HTTP header, in seconds)
            const httpRetryAfter = responseHeaders.get('retry-after');
            if (httpRetryAfter) {
                retryAfter = parseFloat(httpRetryAfter);
            } else {
                // Try to get from 'x-ratelimit-reset-after' (Discord specific, in seconds with fractional)
                const discordRetryAfter = responseHeaders.get('x-ratelimit-reset-after');
                if (discordRetryAfter) {
                    retryAfter = parseFloat(discordRetryAfter);
                }
            }
        }
        // Ensure retryAfter is a sensible number, not NaN, and add a small buffer
        retryAfter = isNaN(retryAfter) ? 1 : Math.max(0.5, retryAfter) + 0.25; // Minimum 0.5s + buffer

        const waitMs = retryAfter * 1000;
        updateStatus(`<span style="color: orange;">Rate limited. Waiting ${retryAfter.toFixed(2)}s...</span>`, true);
        console.warn(`[Disappear] Rate limited. Waiting ${waitMs}ms (derived from headers: retry-after or x-ratelimit-reset-after)`);
        return new Promise(resolve => setTimeout(resolve, waitMs));
    }

    function convertToCSV(dataArray) {
        if (!dataArray || dataArray.length === 0) {
            return "";
        }
        const headers = Object.keys(dataArray[0]);
        const csvRows = [];
        csvRows.push(headers.join(',')); // Add header row

        for (const row of dataArray) {
            const values = headers.map(header => {
                let cell = row[header];
                if (cell === null || cell === undefined) {
                    cell = "";
                } else if (typeof cell === 'object') {
                    cell = JSON.stringify(cell); // Stringify arrays/objects like attachments/embeds
                } else {
                    cell = String(cell);
                }
                // Escape double quotes by doubling them, and wrap if it contains comma, double quote, or newline
                if (cell.includes('"') || cell.includes(',') || cell.includes('\n') || cell.includes('\r')) {
                    cell = `"${cell.replace(/"/g, '""')}"`;
                }
                return cell;
            });
            csvRows.push(values.join(','));
        }
        return csvRows.join('\n');
    }

    function triggerDownload(data, format, filename) {
        let mimeType;
        let fileExtension;
        let content;

        if (format === 'json') {
            mimeType = 'application/json';
            fileExtension = 'json';
            content = JSON.stringify(data, null, 2);
        } else if (format === 'csv') {
            mimeType = 'text/csv';
            fileExtension = 'csv';
            content = convertToCSV(data);
        } else {
            console.error("[Disappear] Unsupported download format:", format);
            updateStatus(`<span style="color: red;">Unsupported download format: ${format}</span>`, true);
            return;
        }

        const blob = new Blob([content], { type: mimeType });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `${filename}.${fileExtension}`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
        updateStatus(`Download started for ${filename}.${fileExtension}`, true);
    }

    // --- UI Creation and Management ---
    function updateStatus(message, append = false) {
        if (statusDiv) {
            if (append) {
                const timestamp = new Date().toLocaleTimeString();
                statusDiv.innerHTML += `<div>[${timestamp}] ${message}</div>`;
                statusDiv.scrollTop = statusDiv.scrollHeight; // Scroll to bottom
            } else {
                statusDiv.innerHTML = message;
            }
        }
        console.log('[Disappear Status]', message.replace(/<[^>]*>?/gm, '')); // Log cleaned message
    }

    function addControlButton() {
        controlButton = document.createElement('button');
        controlButton.innerHTML = '🗑️ Disappear';
        controlButton.id = `${SCRIPT_PREFIX}-control-button`;
        // Basic styling
        controlButton.style.position = 'fixed';
        controlButton.style.top = '15px';
        controlButton.style.right = '15px';
        controlButton.style.zIndex = '9999';
        controlButton.style.backgroundColor = '#7289da'; // Discord blurple
        controlButton.style.color = 'white';
        controlButton.style.border = 'none';
        controlButton.style.padding = '10px 15px';
        controlButton.style.borderRadius = '5px';
        controlButton.style.cursor = 'pointer';
        controlButton.title = 'Open Disappear Options'; // Tooltip

        controlButton.onclick = showModal;
        document.body.appendChild(controlButton);
    }

    function createModalUI(channels) {
        if (modalContainer) modalContainer.remove();

        modalContainer = document.createElement('div');
        modalContainer.id = `${SCRIPT_PREFIX}-modal`;
        // Basic styling (can be improved with GM_addStyle)
        modalContainer.style.position = 'fixed';
        modalContainer.style.top = '50%';
        modalContainer.style.left = '50%';
        modalContainer.style.transform = 'translate(-50%, -50%)';
        modalContainer.style.backgroundColor = '#36393f'; // Discord's dark theme color
        modalContainer.style.padding = '20px';
        modalContainer.style.borderRadius = '8px';
        modalContainer.style.zIndex = '10000';
        modalContainer.style.color = 'white';
        modalContainer.style.maxHeight = '80vh';
        modalContainer.style.overflowY = 'auto';
        modalContainer.style.border = '1px solid #202225';


        modalContainer.innerHTML = `
            <h2 style="margin-top:0; border-bottom: 1px solid #4f545c; padding-bottom:10px;">Disappear by Vili (vili.dev)</h2>
            <p><strong>Warning:</strong> This tool permanently deletes messages. Use with caution. Sharing your auth token is risky.</p>
            
            <div style="margin-bottom: 15px;">
                <label for="${SCRIPT_PREFIX}-auth-token">Auth Token (Required):</label>
                <input type="password" id="${SCRIPT_PREFIX}-auth-token" value="${authToken || ''}" style="width: 100%; padding: 8px; background-color: #202225; border: 1px solid #000; color: white; border-radius: 3px; margin-top:5px;">
                <small>Find this via Inspect Element -> Application -> Local Storage, find discord.com, look for 'token'. Be careful!</small>
            </div>

            <div style="margin-bottom: 15px;">
                <label for="${SCRIPT_PREFIX}-min-delay">Min Delay (ms):</label>
                <input type="number" id="${SCRIPT_PREFIX}-min-delay" value="${MIN_DELAY_MS}" style="width: 80px; padding: 8px; background-color: #202225; border: 1px solid #000; color: white; border-radius: 3px;">
                <label for="${SCRIPT_PREFIX}-max-delay" style="margin-left:10px;">Max Delay (ms):</label>
                <input type="number" id="${SCRIPT_PREFIX}-max-delay" value="${MAX_DELAY_MS}" style="width: 80px; padding: 8px; background-color: #202225; border: 1px solid #000; color: white; border-radius: 3px;">
            </div>

            <p>Select channels/DMs/groups to process:</p>
            <button id="${SCRIPT_PREFIX}-select-all" style="margin-bottom:5px;">Select All</button>
            <button id="${SCRIPT_PREFIX}-deselect-all" style="margin-bottom:5px; margin-left:5px;">Deselect All</button>

            <div id="${SCRIPT_PREFIX}-channel-list" style="max-height: 200px; overflow-y: auto; border: 1px solid #4f545c; padding: 10px; background-color: #2f3136; border-radius:3px;">
                ${channels.length > 0 ? channels.map(ch => `
                    <div style="margin-bottom: 5px;">
                        <input type="checkbox" id="${SCRIPT_PREFIX}-ch-${ch.id}" data-channel-id="${ch.id}" data-channel-name="${ch.name}" checked style="margin-right:5px;">
                        <label for="${SCRIPT_PREFIX}-ch-${ch.id}" title="${ch.id}">${ch.name} (${ch.type})</label>
                    </div>
                `).join('') : '<p>No channels loaded yet. Click "Refresh Channels" or ensure token is valid.</p>'}
            </div>
            <br>
            <button id="${SCRIPT_PREFIX}-refresh-channels" style="margin-right: 10px;">Refresh Channels</button>
            <button id="${SCRIPT_PREFIX}-start-button" class="danger ${SCRIPT_PREFIX}-action-button">Start Deletion</button>
            <button id="${SCRIPT_PREFIX}-stop-button" class="${SCRIPT_PREFIX}-action-button" style="display: none;">Stop Process</button>
            <button id="${SCRIPT_PREFIX}-export-button" class="${SCRIPT_PREFIX}-action-button">Export Messages</button>
            <select id="${SCRIPT_PREFIX}-export-format" style="background-color: #202225; color: white; border: 1px solid #000; border-radius:3px;">
                <option value="json">JSON</option>
                <option value="csv">CSV</option>
            </select>
            <button id="${SCRIPT_PREFIX}-close-button" class="${SCRIPT_PREFIX}-action-button" style="float:right;">Close</button>
            <button id="${SCRIPT_PREFIX}-hide-button" class="${SCRIPT_PREFIX}-action-button" style="float:right;">Hide</button>
            <div id="${SCRIPT_PREFIX}-status-container" style="margin-top: 15px; padding:10px; background-color: #2f3136; border-radius:3px; min-height: 50px; max-height:250px; overflow-y:auto; border: 1px solid #000;">
                 <div id="${SCRIPT_PREFIX}-status">Enter Auth Token and click "Refresh Channels".</div>
            </div>
        `;

        document.body.appendChild(modalContainer);
        statusDiv = document.getElementById(`${SCRIPT_PREFIX}-status`); // Initialize statusDiv

        document.getElementById(`${SCRIPT_PREFIX}-hide-button`).onclick = () => {
            if (modalContainer) {
                modalContainer.style.display = 'none';
            }
        };

        document.getElementById(`${SCRIPT_PREFIX}-close-button`).onclick = () => {
            if (isDeleting || isExporting) {
                const processName = isDeleting ? "Deletion" : "Export";
                if (!confirm(`${processName} is in progress. Are you sure you want to close? This will stop the current process.`)) {
                    return;
                }
                stopCurrentOperation(); // This will set the appropriate flag to false
            }
            if (modalContainer) {
                modalContainer.remove();
                modalContainer = null; // Explicitly nullify to ensure fresh creation next time
            }
        };
        document.getElementById(`${SCRIPT_PREFIX}-start-button`).onclick = startDeletionProcess;
        document.getElementById(`${SCRIPT_PREFIX}-stop-button`).onclick = stopCurrentOperation;
        document.getElementById(`${SCRIPT_PREFIX}-export-button`).onclick = startExportProcess;
        document.getElementById(`${SCRIPT_PREFIX}-refresh-channels`).onclick = async () => {
            authToken = document.getElementById(`${SCRIPT_PREFIX}-auth-token`).value.trim();
             if (!authToken) {
                updateStatus('<span style="color: red;">Please enter Auth Token first!</span>', true);
                return;
            }
            localStorage.setItem(`${SCRIPT_PREFIX}_authToken`, authToken);
            try {
                await fetchAuthenticatedUser(); // Fetch user ID first
                const fetchedChannels = await fetchChannels();
                // Re-render the channel list part of the modal
                const channelListDiv = document.getElementById(`${SCRIPT_PREFIX}-channel-list`);
                if (channelListDiv) {
                    channelListDiv.innerHTML = fetchedChannels.length > 0 ? fetchedChannels.map(ch => `
                        <div style="margin-bottom: 5px;">
                            <input type="checkbox" id="${SCRIPT_PREFIX}-ch-${ch.id}" data-channel-id="${ch.id}" data-channel-name="${ch.name}" checked style="margin-right:5px;">
                            <label for="${SCRIPT_PREFIX}-ch-${ch.id}" title="${ch.id}">${ch.name} (${ch.type})</label>
                        </div>
                    `).join('') : '<p>No channels found or unable to fetch.</p>';
                }
                 updateStatus(`Fetched ${fetchedChannels.length} channels. Ready to select and start.`, true);
            } catch (error) {
                updateStatus(`<span style="color: red;">Error refreshing channels: ${error.message}</span>`, true);
            }
        };

        document.getElementById(`${SCRIPT_PREFIX}-select-all`).onclick = () => {
            document.querySelectorAll(`#${SCRIPT_PREFIX}-channel-list input[type="checkbox"]`).forEach(cb => cb.checked = true);
        };
        document.getElementById(`${SCRIPT_PREFIX}-deselect-all`).onclick = () => {
            document.querySelectorAll(`#${SCRIPT_PREFIX}-channel-list input[type="checkbox"]`).forEach(cb => cb.checked = false);
        };


        // Load saved token if available
        const savedToken = localStorage.getItem(`${SCRIPT_PREFIX}_authToken`);
        if (savedToken) {
            document.getElementById(`${SCRIPT_PREFIX}-auth-token`).value = savedToken;
            authToken = savedToken; // Update global authToken
        }
         // Load saved delays
        MIN_DELAY_MS = parseInt(localStorage.getItem(`${SCRIPT_PREFIX}_minDelay`) || MIN_DELAY_MS);
        MAX_DELAY_MS = parseInt(localStorage.getItem(`${SCRIPT_PREFIX}_maxDelay`) || MAX_DELAY_MS);
        document.getElementById(`${SCRIPT_PREFIX}-min-delay`).value = MIN_DELAY_MS;
        document.getElementById(`${SCRIPT_PREFIX}-max-delay`).value = MAX_DELAY_MS;
    }

    function showModal() {
        if (modalContainer && document.body.contains(modalContainer)) {
            modalContainer.style.display = 'block';
            // Ensure statusDiv is valid if the modal is being reshown
            if (!statusDiv) {
                statusDiv = document.getElementById(`${SCRIPT_PREFIX}-status`);
            }
        } else {
            // If modalContainer was removed (by close button or never created)
            // or if it exists in variable but not in DOM (e.g., edge case)
            if (modalContainer) { // Clean up if it exists but isn't in DOM, or to be certain
                modalContainer.remove();
            }
            createModalUI([]); // Create it fresh, this also initializes statusDiv
            // createModalUI already sets an initial status message.
            // If a different one is needed here, it can be set.
            // For now, createModalUI's default is fine.
        }
    }

    async function startDeletionProcess() {
        if (isDeleting) {
            updateStatus('<span style="color: orange;">Deletion is already in progress.</span>', true);
            return;
        }

        authToken = document.getElementById(`${SCRIPT_PREFIX}-auth-token`).value.trim();
        if (!authToken) {
            updateStatus('<span style="color: red;">Auth Token is required!</span>', true);
            return;
        }
        localStorage.setItem(`${SCRIPT_PREFIX}_authToken`, authToken);

        MIN_DELAY_MS = parseInt(document.getElementById(`${SCRIPT_PREFIX}-min-delay`).value) || 1000;
        MAX_DELAY_MS = parseInt(document.getElementById(`${SCRIPT_PREFIX}-max-delay`).value) || 3000;
        if (MIN_DELAY_MS < 200) MIN_DELAY_MS = 200; // Safety floor
        if (MAX_DELAY_MS < MIN_DELAY_MS) MAX_DELAY_MS = MIN_DELAY_MS + 200;
        localStorage.setItem(`${SCRIPT_PREFIX}_minDelay`, MIN_DELAY_MS);
        localStorage.setItem(`${SCRIPT_PREFIX}_maxDelay`, MAX_DELAY_MS);

        const selectedChannelElements = Array.from(document.querySelectorAll(`#${SCRIPT_PREFIX}-channel-list input[type="checkbox"]:checked`));
        const channelsToProcess = selectedChannelElements.map(el => ({
            id: el.dataset.channelId,
            name: el.dataset.channelName
        }));

        if (channelsToProcess.length === 0) {
            updateStatus('<span style="color: orange;">No channels selected for deletion.</span>', true);
            return;
        }

        if (!confirm(`Are you sure you want to delete ALL YOUR MESSAGES in ${channelsToProcess.length} selected channel(s)/DM(s)? This action is IRREVERSIBLE.`)) {
            updateStatus('Deletion cancelled by user.', true);
            return;
        }

        isDeleting = true;
        updateControlButtonIndicator(true); // Indicate working
        document.getElementById(`${SCRIPT_PREFIX}-start-button`).disabled = true;
        document.getElementById(`${SCRIPT_PREFIX}-stop-button`).style.display = 'inline-block';
        document.getElementById(`${SCRIPT_PREFIX}-refresh-channels`).disabled = true;
        document.getElementById(`${SCRIPT_PREFIX}-export-button`).disabled = true;

        // Clear previous logs and start fresh for this operation session
        if (statusDiv) statusDiv.innerHTML = '';
        updateStatus(`Starting deletion process for ${channelsToProcess.length} channel(s)...`, true);
        console.log('[Disappear]', 'Selected channels for deletion:', channelsToProcess.map(c => c.name));

        try {
            if (!currentUserId) {
                await fetchAuthenticatedUser();
            }
            if (!currentUserId) {
                 throw new Error("Could not obtain User ID. Cannot proceed.");
            }

            for (const channel of channelsToProcess) {
                if (!isDeleting) {
                    updateStatus('Deletion stopped by user during channel iteration.', true);
                    break;
                }
                updateStatus(`--- Starting channel: ${channel.name} (ID: ${channel.id}) ---`, true);
                await processChannel(channel.id, {});
                if (isDeleting) {
                    updateStatus(`--- Finished channel: ${channel.name} ---`, true);
                }
            }
        } catch (error) {
            console.error('[Disappear] Critical error during deletion process:', error);
            updateStatus(`<span style="color: red;">A critical error occurred: ${error.message}. Process halted. Check console.</span>`, true);
        } finally {
            if (isDeleting) { // This means it ran to completion or an error occurred while still 'isDeleting'
                updateStatus('Deletion process run completed. Check logs for details on each channel.', true);
            } else if (!isDeleting && channelsToProcess.length > 0) { // This means it was stopped by user
                 updateStatus('Deletion process was stopped by user or an early error. Check logs.', true);
            }
            isDeleting = false;
            updateControlButtonIndicator(false); // Revert indicator
            const startButton = document.getElementById(`${SCRIPT_PREFIX}-start-button`);
            if (startButton) startButton.disabled = false;
            const stopButton = document.getElementById(`${SCRIPT_PREFIX}-stop-button`);
            if (stopButton) stopButton.style.display = 'none';
            const refreshButton = document.getElementById(`${SCRIPT_PREFIX}-refresh-channels`);
            if (refreshButton) refreshButton.disabled = false;
            const exportButton = document.getElementById(`${SCRIPT_PREFIX}-export-button`);
            if (exportButton) exportButton.disabled = false;
        }
    }

    function stopCurrentOperation() {
        if (isDeleting) {
            isDeleting = false;
            updateStatus('<span style="color: orange;">Stopping deletion process... Please wait for current operations to finish.</span>', true);
            console.log('[Disappear] Stop command received for DELETION. Process will halt after the current message/batch.');
        } else if (isExporting) {
            isExporting = false;
            updateStatus('<span style="color: orange;">Stopping export process... Please wait for current fetch to finish.</span>', true);
            console.log('[Disappear] Stop command received for EXPORT. Process will halt after the current fetch segment.');
        } else {
            updateStatus('No process is currently running.');
        }
        // UI updates for buttons are primarily handled in the finally blocks of startDeletionProcess/startExportProcess
        // However, we can ensure the stop button is hidden if no process was found to be running.
        if (!isDeleting && !isExporting) {
            const stopButton = document.getElementById(`${SCRIPT_PREFIX}-stop-button`);
            if (stopButton) stopButton.style.display = 'none';
            // Also ensure other buttons are enabled if modal is open
            const startButton = document.getElementById(`${SCRIPT_PREFIX}-start-button`);
            if (startButton) startButton.disabled = false;
            const exportButton = document.getElementById(`${SCRIPT_PREFIX}-export-button`);
            if (exportButton) exportButton.disabled = false;
            const refreshButton = document.getElementById(`${SCRIPT_PREFIX}-refresh-channels`);
            if (refreshButton) refreshButton.disabled = false;
        }
    }

    async function startExportProcess() {
        if (isDeleting) {
            updateStatus('<span style="color: orange;">Deletion is in progress. Please stop it before exporting.</span>', true);
            return;
        }
        if (isExporting) {
            updateStatus('<span style="color: orange;">Export is already in progress.</span>', true);
            return;
        }

        authToken = document.getElementById(`${SCRIPT_PREFIX}-auth-token`).value.trim();
        if (!authToken) {
            updateStatus('<span style="color: red;">Auth Token is required!</span>', true);
            return;
        }
        localStorage.setItem(`${SCRIPT_PREFIX}_authToken`, authToken);

        MIN_DELAY_MS = parseInt(document.getElementById(`${SCRIPT_PREFIX}-min-delay`).value) || 1000;
        MAX_DELAY_MS = parseInt(document.getElementById(`${SCRIPT_PREFIX}-max-delay`).value) || 3000;
        localStorage.setItem(`${SCRIPT_PREFIX}_minDelay`, MIN_DELAY_MS);
        localStorage.setItem(`${SCRIPT_PREFIX}_maxDelay`, MAX_DELAY_MS);

        const selectedChannelElements = Array.from(document.querySelectorAll(`#${SCRIPT_PREFIX}-channel-list input[type="checkbox"]:checked`));
        const channelsToExport = selectedChannelElements.map(el => ({
            id: el.dataset.channelId,
            name: el.dataset.channelName
        }));

        if (channelsToExport.length === 0) {
            updateStatus('<span style="color: orange;">No channels selected for export.</span>', true);
            return;
        }

        const exportFormat = document.getElementById(`${SCRIPT_PREFIX}-export-format`).value;

        if (!confirm(`Are you sure you want to fetch and export ALL YOUR MESSAGES from ${channelsToExport.length} selected channel(s)/DM(s) as ${exportFormat.toUpperCase()}? This might take a while and generate many API requests.`)) {
            updateStatus('Export cancelled by user.', true);
            return;
        }

        isExporting = true;
        updateControlButtonIndicator(true); // Indicate working
        document.getElementById(`${SCRIPT_PREFIX}-start-button`).disabled = true;
        document.getElementById(`${SCRIPT_PREFIX}-export-button`).disabled = true;
        document.getElementById(`${SCRIPT_PREFIX}-stop-button`).style.display = 'inline-block';
        document.getElementById(`${SCRIPT_PREFIX}-refresh-channels`).disabled = true;

        // Clear previous logs and start fresh for this operation session
        if (statusDiv) statusDiv.innerHTML = '';
        updateStatus(`Starting export process for ${channelsToExport.length} channel(s) as ${exportFormat.toUpperCase()}...`, true);
        console.log('[Disappear]', 'Selected channels for export:', channelsToExport.map(c => c.name));

        let allExportedMessages = [];

        try {
            if (!currentUserId) {
                await fetchAuthenticatedUser();
            }
            if (!currentUserId) {
                throw new Error("Could not obtain User ID. Cannot proceed with export.");
            }

            for (const channel of channelsToExport) {
                if (!isExporting) {
                    updateStatus('Export stopped by user during channel iteration.', true);
                    break;
                }
                updateStatus(`--- Starting export for channel: ${channel.name} (ID: ${channel.id}) ---`, true);
                const { messages: channelMessages, skipped, error: channelError } = await fetchAllUserMessagesForExport(channel.id, currentUserId, channel.name);

                if (skipped && channelError) {
                    updateStatus(`<span style="color: red;">--- Skipped channel ${channel.name} due to errors during export. ---</span>`, true);
                } else if (skipped) {
                    updateStatus(`--- Channel ${channel.name} was skipped (e.g. no access or stopped). ---`, true);
                } else if (channelMessages.length > 0) {
                    allExportedMessages.push(...channelMessages);
                    updateStatus(`--- Finished export for channel: ${channel.name}. Fetched ${channelMessages.length} messages. Total: ${allExportedMessages.length} ---`, true);
                } else if (isExporting) {
                    updateStatus(`--- Finished export for channel: ${channel.name}. No new messages found or fetched for this channel. ---`, true);
                }
            }

            if (isExporting && allExportedMessages.length > 0) {
                updateStatus(`Total messages fetched from all selected channels: ${allExportedMessages.length}. Preparing download...`, true);
                const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').slice(0, -5);
                triggerDownload(allExportedMessages, exportFormat, `discord_messages_export_${timestamp}`);
            } else if (isExporting) {
                updateStatus('No messages found to export from any of the selected channels after processing all.', true);
            }

        } catch (error) {
            console.error('[Disappear] Critical error during export process:', error);
            updateStatus(`<span style="color: red;">A critical error occurred during export: ${error.message}. Process halted. Check console.</span>`, true);
        } finally {
            if (isExporting) {
                updateStatus('Export process run completed. Check logs for details.', true);
            } else if (!isExporting && channelsToExport.length > 0) {
                updateStatus('Export process was stopped by user or an early error. Check logs.', true);
            }
            isExporting = false;
            updateControlButtonIndicator(false); // Revert indicator
            const startButton = document.getElementById(`${SCRIPT_PREFIX}-start-button`);
            if (startButton) startButton.disabled = false;
            const exportButton = document.getElementById(`${SCRIPT_PREFIX}-export-button`);
            if (exportButton) exportButton.disabled = false;
            const stopButton = document.getElementById(`${SCRIPT_PREFIX}-stop-button`);
            if (stopButton) stopButton.style.display = 'none';
            const refreshButton = document.getElementById(`${SCRIPT_PREFIX}-refresh-channels`);
            if (refreshButton) refreshButton.disabled = false;
        }
    }

    // --- Initialization ---
    function init() {
        console.log('[Disappear]', 'Script loaded.');
        addControlButton();

        // We need a way to get the user's own ID for message filtering.
        // This often involves listening to network requests or inspecting internal Discord objects.
        // For Undiscord, it seems to get it from an API endpoint or a global variable.
        // We'll need to investigate how to reliably get the current user's ID.
        // We will try to fetch this when the user clicks "Refresh Channels" or "Start Deletion" after providing token.

        GM_addStyle(`
            #${SCRIPT_PREFIX}-modal button.danger { background-color: #f04747; color: white; } /* Default, will be more specific below */
            #${SCRIPT_PREFIX}-modal button.danger:hover { background-color: #d84040; } /* Default, will be more specific below */
            #${SCRIPT_PREFIX}-modal input[type="checkbox"] { transform: scale(1.2); margin-right: 8px; vertical-align: middle;}
            #${SCRIPT_PREFIX}-status-container div { margin-bottom: 4px; font-size: 0.9em;}
            #${SCRIPT_PREFIX}-status-container div:last-child { margin-bottom: 0;}
            
            @keyframes disappearPulse {
                0% { box-shadow: 0 0 0 0 rgba(114, 137, 218, 0.7); }
                70% { box-shadow: 0 0 0 10px rgba(114, 137, 218, 0); }
                100% { box-shadow: 0 0 0 0 rgba(114, 137, 218, 0); }
            }
            .${SCRIPT_PREFIX}-working-indicator {
                animation: disappearPulse 2s infinite;
            }

            /* Base style for main action buttons in the modal */
            #${SCRIPT_PREFIX}-modal .${SCRIPT_PREFIX}-action-button {
                padding: 7px 14px; /* Uniform padding */
                border-radius: 3px;
                border: 1px solid transparent;
                cursor: pointer;
                margin-left: 8px; /* Uniform margin */
                min-width: 110px; /* Minimum width for consistency */
                box-sizing: border-box;
                text-align: center;
                vertical-align: middle;
                font-size: 14px;
            }

            /* Start Deletion Button (uses .danger class) */
            #${SCRIPT_PREFIX}-modal button.danger.${SCRIPT_PREFIX}-action-button {
                background-color: #f04747;
                color: white;
                border-color: #f04747;
            }
            #${SCRIPT_PREFIX}-modal button.danger.${SCRIPT_PREFIX}-action-button:hover {
                background-color: #d84040;
                border-color: #d84040;
            }
            #${SCRIPT_PREFIX}-modal button.danger.${SCRIPT_PREFIX}-action-button:disabled {
                background-color: #747f8d; /* Discord's disabled button grey */
                color: #dcddde;       /* Discord's disabled button text color */
                border-color: #747f8d;
                cursor: not-allowed;
            }

            /* Stop Button */
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-stop-button.${SCRIPT_PREFIX}-action-button {
                background-color: #faa61a; /* Orange/Warning */
                color: white;
                border-color: #faa61a;
            }
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-stop-button.${SCRIPT_PREFIX}-action-button:hover {
                background-color: #e79817;
                border-color: #e79817;
            }

            /* Export Button */
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-export-button.${SCRIPT_PREFIX}-action-button {
                background-color: #43b581; /* Green */
                color: white;
                border-color: #43b581;
            }
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-export-button.${SCRIPT_PREFIX}-action-button:hover {
                background-color: #3aa873;
                border-color: #3aa873;
            }
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-export-button.${SCRIPT_PREFIX}-action-button:disabled {
                background-color: #3a7056; /* Muted green for disabled */
                color: #a0c3b0;
                border-color: #3a7056;
                cursor: not-allowed;
            }

            /* Hide Button */
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-hide-button.${SCRIPT_PREFIX}-action-button {
                background-color: #5865f2; /* Blurple */
                color: white;
                border-color: #5865f2;
            }
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-hide-button.${SCRIPT_PREFIX}-action-button:hover {
                background-color: #4a55cf;
                border-color: #4a55cf;
            }

            /* Close Button */
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-close-button.${SCRIPT_PREFIX}-action-button {
                background-color: #72767d; /* Discord's secondary/grey button */
                color: white;
                border-color: #72767d;
            }
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-close-button.${SCRIPT_PREFIX}-action-button:hover {
                background-color: #686c72;
                border-color: #686c72;
            }
            
            /* Styling for the export format select element */
            #${SCRIPT_PREFIX}-modal #${SCRIPT_PREFIX}-export-format {
                padding: 7px;
                background-color: #202225;
                color: white;
                border: 1px solid #000;
                border-radius: 3px;
                vertical-align: middle;
                margin-left: 8px;
                font-size: 14px;
            }
        `);
    }

    // Wait for Discord to load before initializing
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();