AO3 FicTracker

Track your favorite, finished, to-read and disliked fanfics on AO3 with sync across devices. Customizable tags and highlights make it easy to manage and spot your tracked works. Full UI customization on the preferences page.

// ==UserScript==
// @name         AO3 FicTracker
// @author       infiniMotis
// @version      1.6.2
// @namespace    https://github.com/infiniMotis/AO3-FicTracker
// @description  Track your favorite, finished, to-read and disliked fanfics on AO3 with sync across devices. Customizable tags and highlights make it easy to manage and spot your tracked works. Full UI customization on the preferences page.
// @license      GNU GPLv3
// @icon         https://archiveofourown.org/favicon.ico
// @match        *://archiveofourown.org/*
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// @require      https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js
// @supportURL   https://github.com/infiniMotis/AO3-FicTracker/issues
// @contributionURL https://ko-fi.com/infinimotis
// @contributionAmount 1 USD
// ==/UserScript==


// Description:
// FicTracker is designed for you to effectively manage their fanfics on AO3.
// It allows you to mark fics as finished, favorite, to-read, or disliked, providing an easy way to organize their reading list.

// Key Features:
// **Custom "To-Read" Feature:** Users can filter and search through their to-read list, enhancing the experience beyond AO3's default functionality.
// **Data Synchronization:** Information is linked to the user's AO3 account, enabling seamless syncing across devices.
// **Google Sheets Storage Sync:** Syncs advanced tracking data such as highlighting and custom notes across multiple devices using a Google Sheets document.
// **User-Friendly Access:** Users can conveniently access tracking options from a dropdown menu, making the process intuitive and straightforward.
// **Optimized performance:** The script runs features only on relevant pages, ensuring quick and efficient performance.

// Usage Instructions:
// 1. **Tracking Fics:** On the fics page, click the status button, on search result/fics listing pages - in the right bottom corner of each work there is a dropdown.
// 2. **Settings Panel:** At the end of the user preferences page, you will find a settings panel to customize your tracking options.
// 3. **Accessing Your Lists:** In the dropdown menu at the top right corner, you'll find links to your tracked lists for easy access.
// 4. **Multi-Device Sync (Optional):**
//    - On your main device, initialize Google Sheets storage via the settings panel.
//    - On other devices, use the same Sheet URL and initialize - data will sync automatically.
//    - Highlighting and custom notes will sync across devices using this feature.

(function() {
    'use strict';

    // Default script settings
    let settings = {
        version: GM_info.script.version,
        statuses: [
            {
                tag: 'Finished Reading',
                dropdownLabel: 'My Finished Fanfics',
                positiveLabel: '✔️ Mark as Finished',
                negativeLabel: '🗑️ Remove from Finished',
                selector: 'finished_reading_btn',
                storageKey: 'FT_finished',
                enabled: true,
                collapse: false,
                displayInDropdown: true,
                highlightColor: "#000",
                borderSize: 0,
                opacity: .6,
                hide: false
            },
            {
                tag: 'Favorite',
                dropdownLabel: 'My Favorite Fanfics',
                positiveLabel: '❤️ Mark as Favorite',
                negativeLabel: '💔 Remove from Favorites',
                selector: 'favorite_btn',
                storageKey: 'FT_favorites',
                enabled: true,
                collapse: false,
                displayInDropdown: true,
                highlightColor: "#F95454",
                borderSize: 2,
                opacity: 1,
                hide: false
            },
            {
                tag: 'To Read',
                dropdownLabel: 'My To Read Fanfics',
                positiveLabel: '📚 Mark as To Read',
                negativeLabel: '🧹 Remove from To Read',
                selector: 'to_read_btn',
                storageKey: 'FT_toread',
                enabled: true,
                collapse: false,
                displayInDropdown: true,
                highlightColor: "#3BA7C4",
                borderSize: 2,
                opacity: 1,
                hide: false
            },
            {
                tag: 'Disliked Work',
                dropdownLabel: 'My Disliked Fanfics',
                positiveLabel: '👎 Mark as Disliked',
                negativeLabel: '🧹 Remove from Disliked',
                selector: 'disliked_btn',
                storageKey: 'FT_disliked',
                enabled: true,
                collapse: true,
                displayInDropdown: true,
                highlightColor: "#000",
                borderSize: 0,
                opacity: .6,
                hide: false
            }
        ],
        loadingLabel: '⏳Loading...',
        hideDefaultToreadBtn: true,
        newBookmarksPrivate: true,
        newBookmarksRec: false,
        lastExportTimestamp: null,
        displayBottomActionButtons: true,
        deleteEmptyBookmarks: true,
        debug: false,
        displayUserNotes: true,
        displayUserNotesBtn: true,
        expandUserNoteDetails: true,
        sheetUrl: "",
        syncInterval: 60,
        syncEnabled: false,
        syncDBInitialized: false,
        syncWidgetEnabled: true,
        syncWidgetOpacity: .5,
        exportStatusesConfig: true,
    };

    // Toggle debug info
    let DEBUG = settings.debug;

    // Utility function for status settings retrieval
    function getStatusSettingsByStorageKey(storageKey) {
        return settings.statuses.find(status => status.storageKey === storageKey);
    }

    // Utility function for displaying modals
    function displayModal(modalTitle, htmlContent) {
        // Check if temp-content already exists, remove if yes (to avoid duplicates)
        const existing = document.getElementById('temp-content');
        if (existing) existing.remove();

        // Create hidden container
        const tempDiv = document.createElement('div');
        tempDiv.id = 'temp-content';
        tempDiv.style.display = 'none';
        tempDiv.innerHTML = htmlContent;

        document.body.appendChild(tempDiv);

        // Show modal using ao3modal
        ao3modal.show('#temp-content', modalTitle);
    }

    // Utility class for injecting CSS
    class StyleManager {
        // Method to add custom styles to the page
        static addCustomStyles(styles) {
            const customStyle = document.createElement('style');
            customStyle.innerHTML = styles;
            document.head.appendChild(customStyle);

            DEBUG && console.info('[FicTracker] Custom styles added successfully.');
        }

        static generateStatusStyles() {
            let css = '';

            settings.statuses.forEach(status => {
                if (!status.enabled) return;

                const className = `glowing-border-${status.storageKey}`;
                const color = status.highlightColor;
                const border = `${status.borderSize}px solid ${color}`;
                const boxShadow = `0 0 10px ${color}, 0 0 20px ${color}`;
                const boxShadowHover = `0 0 15px ${color}, 0 0 30px ${color}`;
                const opacity = status.opacity;
                const hasBorder = status.borderSize > 0;
                const hide = status.hide;

                css += `
                    .${className} {
                        ${hide ? 'display: none !important;' : ''}
                        ${hasBorder ? `border: ${border} !important;` : 'border: none !important;'}
                        border-radius: 8px !important;
                        padding: 15px !important;
                        background-color: transparent !important;
                        ${hasBorder ? `box-shadow: ${boxShadow} !important;` : 'box-shadow: none !important;'}
                        transition: box-shadow 0.3s ease, opacity 0.3s ease !important;
                        opacity: ${opacity};
                    }
                    .${className}:hover {
                        ${hasBorder ? `box-shadow: ${boxShadowHover} !important;` : 'box-shadow: none !important;'}
                        opacity: 1;
                    }
                `;

            });

            return css;
        }
    }

    // Class for handling API requests
    class RequestManager {
        constructor(baseApiUrl) {
            this.baseApiUrl = baseApiUrl;
        }

        // Retrieve the authenticity token from a meta tag
        getAuthenticityToken() {
            const metaTag = document.querySelector('meta[name="csrf-token"]');
            return metaTag ? metaTag.getAttribute('content') : null;
        }

        // Send an API request with the specified method
        sendRequest(url, formData = null, headers = null, method = "POST") {
            const options = {
                method: method,
                mode: "cors",
                credentials: "include",
            };

            // Attach headers if there are any
            if (headers) {
                options.headers = headers;
            }

            // If it's not a GET request, we include the formData in the request body
            if (method !== "GET" && formData) {
                options.body = formData;
            }

            return fetch(url, options)
                .then(response => {
                    if (!response.ok) {
                        throw new Error(`Request failed with status ${response.status}`);
                    }
                    return response;
                })
                .catch(error => {
                    DEBUG && console.error('[FicTracker] Error during API request:', error);
                    throw error;
                });
        }

        // Create a bookmark for fanfic with given data
        createBookmark(workId, authenticityToken, bookmarkData) {
            const url = `${this.baseApiUrl}/works/${workId}/bookmarks`;
            const headers = this.getRequestHeaders();
            const formData = this.createFormData(authenticityToken, bookmarkData);

            DEBUG && console.info('[FicTracker] Sending CREATE request for bookmark:', {
                url,
                headers,
                bookmarkData
            });

            return this.sendRequest(url, formData, headers)
                .then(response => {
                    if (response.ok) {
                        const bookmarkId = response.url.split('/').pop();

                        DEBUG && console.log('[FicTracker] Created bookmark ID:', bookmarkId);
                        return bookmarkId;
                    } else {
                        throw new Error("Failed to create bookmark. Status: " + response.status);
                    }
                })
                .catch(error => {
                    DEBUG && console.error('[FicTracker] Error creating bookmark:', error);
                    throw error;
                });
        }

        // Update a bookmark for fanfic with given data
        updateBookmark(bookmarkId, authenticityToken, updatedData) {
            const url = `${this.baseApiUrl}/bookmarks/${bookmarkId}`;
            const headers = this.getRequestHeaders();
            const formData = this.createFormData(authenticityToken, updatedData, 'update');

            DEBUG && console.info('[FicTracker] Sending UPDATE request for bookmark:', {
                url,
                headers,
                updatedData
            });

            return this.sendRequest(url, formData, headers)
                .then(data => {
                    DEBUG && console.log('[FicTracker] Bookmark updated successfully:', data);
                })
                .catch(error => {
                    DEBUG && console.error('[FicTracker] Error updating bookmark:', error);
                });
        }

        // Delete a bookmark by ID
        deleteBookmark(bookmarkId, authenticityToken) {
            const url = `${this.baseApiUrl}/bookmarks/${bookmarkId}`;
            const headers = this.getRequestHeaders();

            // FormData for this one is minimalist, method call is not needed
            const formData = new FormData();
            formData.append('authenticity_token', authenticityToken);
            formData.append('_method', 'delete');

            DEBUG && console.info('[FicTracker] Sending DELETE request for bookmark:', {
                url,
                headers,
                authenticityToken
            });

            return this.sendRequest(url, formData, headers)
                .then(data => {
                    DEBUG && console.log('[FicTracker] Bookmark deleted successfully:', data);
                })
                .catch(error => {
                    DEBUG && console.error('[FicTracker] Error deleting bookmark:', error);
                });
        }

        // Retrieve the request headers
        getRequestHeaders() {
            const headers = {
                "Accept": "text/html", // Accepted content type
                "Cache-Control": "no-cache", // Prevent caching
                "Pragma": "no-cache", // HTTP 1.0 compatibility
            };


            DEBUG && console.log('[FicTracker] Retrieving request headers:', headers);

            return headers;
        }

        // Create FormData for bookmarking actions based on action type
        createFormData(authenticityToken, bookmarkData, type = 'create') {
            const formData = new FormData();

            // Append required data to FormData
            formData.append('authenticity_token', authenticityToken);
            formData.append("bookmark[pseud_id]", bookmarkData.pseudId);
            formData.append("bookmark[bookmarker_notes]", bookmarkData.notes);
            formData.append("bookmark[tag_string]", bookmarkData.bookmarkTags.join(','));
            formData.append("bookmark[collection_names]", bookmarkData.collections.join(','));
            formData.append("bookmark[private]", +bookmarkData.isPrivate);
            formData.append("bookmark[rec]", +bookmarkData.isRec);

            // Append action type
            formData.append("commit", type === 'create' ? "Create" : "Update");
            if (type === 'update') {
                formData.append("_method", "put");
            }

            DEBUG && console.log('[FicTracker] FormData created successfully:');
            DEBUG && console.table(Array.from(formData.entries()));

            return formData;
        }

    }

    // Class for managing custom user notes
    class CustomUserNotesManager {
        constructor(storageManager, remoteSyncManager = null) {
            this.storageManager = storageManager;
            this.remoteSyncManager = remoteSyncManager;
        }

        // Get all saved notes
        getAllNotes() {
            try {
                return JSON.parse(this.storageManager.getItem("FT_userNotes")) || {};
            } catch (e) {
                return {};
            }
        }

        // Get note for specific work
        getNote(workId) {
            const notes = this.getAllNotes();
            return notes[workId] || null;
        }

        // Save note
        saveNote(workId, noteText) {
            const notes = this.getAllNotes();
            const date = new Date().toISOString();
            
            if (noteText.trim() === "") {
                delete notes[workId];
            } else {
                notes[workId] = {
                    text: noteText,
                    date
                };
            }

            this.storageManager.setItem("FT_userNotes", JSON.stringify(notes));
            
            if (this.remoteSyncManager) {
                this.remoteSyncManager.addPendingNoteUpdate(workId, noteText, date);
            }

            return { text: noteText, date };
        }

        // Delete note
        deleteNote(workId) {
            const notes = this.getAllNotes();
            delete notes[workId];
            this.storageManager.setItem("FT_userNotes", JSON.stringify(notes));
            
            if (this.remoteSyncManager) {
                this.remoteSyncManager.addPendingNoteUpdate(workId, "", null);
            }
        }

        // Generate note block HTML
        generateNoteHtml(workId, isWorkPage = false) {
            const note = this.getNote(workId);
            const noteText = note?.text || '';
            const noteDate = note?.date || '';
            const displayDate = noteDate ? new Date(noteDate).toLocaleDateString() : '';
            const detailsOpen = settings.expandUserNoteDetails ? 'open' : '';

            // If no note exists, show create button
            if (!noteText) {
                return `
                    <div class="user-note-preview" data-work-id="${workId}" style="order: 999; flex-basis: 100%;">
                        <div style="display: flex; justify-content: center; padding: ${isWorkPage ? '10px' : '3px'};">
                            <button class="create-note-btn" style="${isWorkPage ? 'width: 30%;' : ''} padding: 4px 6px; display: flex; justify-content: center; align-items: center; gap: 8px; border: 1px dashed currentColor; border-radius: 4px; background: transparent; color: currentColor; cursor: pointer; opacity: 0.7;">
                                <span style="color: currentColor;">📝</span>
                                <span>Add Note</span>
                            </button>
                        </div>
                    </div>
                `;
            }

            return `
                <div class="user-note-preview" data-work-id="${workId}" style="order: 999; flex-basis: 100%;">
                    <style>
                        @media screen and (max-width: 42em) {
                            .user-note-preview[data-work-id="${workId}"] > div > div {
                                width: 100% !important;
                            }
                        }
                    </style>
                    <div style="display: flex; justify-content: center;">
                        <!-- Config edit form for works listing or fic page itself -->
                        <div style="width: ${isWorkPage ? '60%' : '100%'};">
                            <details ${detailsOpen} style="margin: 18px 0 1px 0;; border: 1px solid currentColor; border-radius: 4px; padding: 0;">
                                <summary style="padding: 4px 6px; cursor: pointer; font-weight: bold; background: rgba(128,128,128,0.1); display: flex; justify-content: space-between; align-items: center;">
                                    <div style="display: flex; align-items: center; gap: 8px;">
                                        <span>📝 Your Note</span>
                                    </div>
                                    <div class="note-actions" style="display: flex; gap: 8px;">
                                        <button class="edit-note-btn" title="Edit Note" style="background: none; border: none; cursor: pointer;">✏️</button>
                                        <button class="delete-note-btn" title="Delete Note" style="background: none; border: none; cursor: pointer;">🗑️</button>
                                    </div>
                                </summary>
                        <div class="note-body" style="padding: 12px; border-top: 1px solid rgba(128,128,128,0.2); background: rgba(128,128,128,0.05);">
                            <div style="line-height: 1.4; white-space: pre-wrap;">${noteText}</div>
                            <div style="margin-top: 8px; font-size: 0.85em; opacity: 0.7;">
                                📅 Last updated: ${displayDate} | 📏 ${noteText.length} characters
                            </div>
                        </div>
                        <div class="note-edit-form" style="display: none; padding: 12px; border-top: 1px solid rgba(128,128,128,0.2); background: rgba(128,128,128,0.05);">
                            <textarea class="note-textarea" style="box-sizing: border-box; width: 100%; min-height: 100px; margin-bottom: 8px; padding: 8px; border: 1px solid rgba(128,128,128,0.2); border-radius: 4px;">${noteText}</textarea>
                            <div style="display: flex; gap: 8px; justify-content: flex-end;">
                                <button class="save-note-btn" style="cursor: pointer;">💾 Save</button>
                                <button class="cancel-edit-btn" style="cursor: pointer;">❌ Cancel</button>
                            </div>
                        </div>
                    </details>
                </div>
            `;
        }

        // Setup event handlers
        setupNoteHandlers(container, isWorkPage = false) {
            container.addEventListener("click", (e) => {
                const noteBlock = e.target.closest(".user-note-preview");
                if (!noteBlock) return;

                const workId = noteBlock.dataset.workId;
                const btn = e.target.closest("button");
                if (!btn) return;

                if (btn.classList.contains("create-note-btn")) {
                    noteBlock.innerHTML = this.generateEditFormHtml(isWorkPage);
                }

                if (btn.classList.contains("edit-note-btn")) {
                    // Prevent details from toggling
                    e.preventDefault();
                    const noteContent = noteBlock.querySelector("details");
                    noteContent.querySelector(".note-body").style.display = "none";
                    noteContent.querySelector(".note-edit-form").style.display = "block";

                    btn.closest('details').open = true;
                }

                if (btn.classList.contains("save-note-btn")) {
                    const textarea = noteBlock.querySelector(".note-textarea");
                    this.saveNote(workId, textarea.value);
                    this.updateNoteDisplay(noteBlock, workId, isWorkPage);
                }

                if (btn.classList.contains("cancel-edit-btn")) {
                    this.updateNoteDisplay(noteBlock, workId, isWorkPage);
                }

                if (btn.classList.contains("delete-note-btn")) {
                    // Prevent details from toggling
                    e.preventDefault();
                    if (confirm("Delete this note?")) {
                        this.deleteNote(workId);
                        this.updateNoteDisplay(noteBlock, workId, isWorkPage);
                    }
                }
            });
        }

        generateEditFormHtml(isWorkPage = false) {
            return `
                <style>
                    @media screen and (max-width: 42em) {
                        .user-note-preview > div > div {
                            width: 100% !important;
                        }
                    }
                </style>
                <div style="display: flex; justify-content: center;">
                    <div style="margin: 18px 0 1px 0; border: 1px solid currentColor; border-radius: 4px; padding: 12px; background: rgba(128,128,128,0.05); box-sizing: border-box !important; width: ${isWorkPage ? '60%' : '100%'};">
                        <textarea class="note-textarea" placeholder="Write your note here..." style="box-sizing: border-box; width: 100%; min-height: 100px; margin-bottom: 8px; padding: 8px; border: 1px solid rgba(128,128,128,0.2); border-radius: 4px;"></textarea>
                        <div style="display: flex; gap: 8px; justify-content: flex-end;">
                            <button class="save-note-btn" style="cursor: pointer;">💾 Save</button>
                            <button class="cancel-edit-btn" style="cursor: pointer;">❌ Cancel</button>
                        </div>
                    </div>
                </div>
            `;
        }


        updateNoteDisplay(noteBlock, workId, isWorkPage = false) {
            noteBlock.outerHTML = this.generateNoteHtml(workId, isWorkPage);
        }
    }

    // Class for managing storage caching
    class StorageManager {
        // Store a value in local storage
        setItem(key, value) {
            localStorage.setItem(key, value);
        }

        // Retrieve a value from local storage
        getItem(key) {
            const value = localStorage.getItem(key);
            return value;
        }

        // Add an ID to a specific category
        addIdToCategory(category, id) {
            const existingIds = this.getItem(category);
            const idsArray = existingIds ? existingIds.split(',') : [];

            if (!idsArray.includes(id)) {
                idsArray.push(id);
                this.setItem(category, idsArray.join(',')); // Update the category with new ID
                DEBUG && console.debug(`[FicTracker] Added ID to category "${category}": ${id}`);
            }
        }

        // Remove an ID from a specific category
        removeIdFromCategory(category, id) {
            const existingIds = this.getItem(category);
            const idsArray = existingIds ? existingIds.split(',') : [];

            const idx = idsArray.indexOf(id);
            if (idx !== -1) {
                idsArray.splice(idx, 1); // Remove the ID
                this.setItem(category, idsArray.join(',')); // Update the category
                DEBUG && console.debug(`[FicTracker] Removed ID from category "${category}": ${id}`);
            }
        }

        // Get IDs from a specific category
        getIdsFromCategory(category) {
            const existingIds = this.getItem(category) || '';
            const idsArray = existingIds.split(',');
            DEBUG && console.debug(`[FicTracker] Retrieved IDs from category "${category}"`);
            return idsArray;
        }
    }

    // Manages syncing data between local storage and a remote backend (google sheets api)
    class RemoteStorageSyncManager {
        constructor() {
            this.storageManager = new StorageManager();
            this.syncedKeys = ['FT_favorites', 'FT_disliked', 'FT_toread', 'FT_finished'];
            this.PENDING_CHANGES_KEY = 'FT_pendingChanges';
            this.LAST_SYNC_KEY = 'FT_lastSync';

            // Configuration
            this.syncInterval = settings.syncInterval * 1000 //seconds
            this.syncTimer = null;
            this.isOnline = navigator.onLine;

            // Floating widget props
            this.syncWidget = null;
            this.timeUntilNextSync = 0;
            this.isSyncing = false;

            // Preserve this context
            this.handleOnline = this.handleOnline.bind(this);
            this.handleOffline = this.handleOffline.bind(this);
            this.handleVisibilityChange = this.handleVisibilityChange.bind(this);

            DEBUG && console.log('[FicTracker] Initialized RemoteStorageSyncManager with syncInterval:', this.syncInterval / 1000, 's');
        }

        // Initialize sync system
        init() {
            // Initialize pending changes storage if not present
            if (!this.storageManager.getItem(this.PENDING_CHANGES_KEY)) {
                this.storageManager.setItem(this.PENDING_CHANGES_KEY, JSON.stringify({
                    operations: [],
                    notes: []
                }));
            }

            DEBUG && console.log('[FicTracker] Pending changes storage initialized');

            // Set up event listeners for (dis)connecting to network, tab focus change
            window.addEventListener('online', this.handleOnline);
            window.addEventListener('offline', this.handleOffline);

            document.addEventListener('visibilitychange', this.handleVisibilityChange);

            // Start sync timer
            this.startSyncTimer();

            // Add widget with timer
            if (settings.syncWidgetEnabled && settings.syncDBInitialized) {
                this.updateSyncWidget();
                setInterval(() => {
                    if (this.timeUntilNextSync > 0) this.timeUntilNextSync--;
                    this.updateSyncWidget(this.isOnline ? (this.isSyncing ? 'syncing' : 'normal') : 'offline');
                }, 1000);
            }
        }

        // Method to create widget and handle all updates
        updateSyncWidget(state = 'normal') {
            if (!settings.syncWidgetEnabled || !settings.syncDBInitialized) return;

            // create widget if it doesn't exist
            if (!this.syncWidget) {
                const mobile = window.innerWidth <= 768;

                document.body.insertAdjacentHTML('beforeend', `
                    <div id="ft-sync-widget" style="position:fixed;bottom:15px;left:10px;z-index:10000;display:flex;align-items:center; opacity: ${settings.syncWidgetOpacity};gap:${mobile?'2px':'4px'};padding:${mobile?'2px 3px':'3px 5px'};background:#fff;border:1px solid #ddd;border-radius:${mobile?'10px':'16px'};cursor:pointer;font:${mobile?'11px':'12px'} -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#666;box-shadow:0 2px 8px rgba(0,0,0,0.1);transition:all 0.2s;user-select:none">
                        <svg width="${mobile?'12':'14'}" height="${mobile?'12':'14'}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="transition:transform 0.3s">
                            <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/>
                        </svg>
                        <span style="font-weight:500;">Sync</span>
                        <span id="ft-sync-badge" style="display:none;background:#ff9800;color:white;border-radius:6px;padding:1px ${mobile?'3px':'5px'};font-size:${mobile?'9px':'10px'};font-weight:bold;margin-left:2px">0</span>
                    </div>
                `);

                this.syncWidget = document.getElementById('ft-sync-widget');
                this.syncBadge = document.getElementById('ft-sync-badge');

                // spin animation
                if (!document.getElementById('ft-spin')) {
                    document.head.insertAdjacentHTML('beforeend', '<style id="ft-spin">@keyframes ft-spin{to{transform:rotate(360deg)}}</style>');
                }

                // click handler
                this.syncWidget.onclick = () => this.isOnline && !this.isSyncing && this.performSync();

                // hover effect
                this.syncWidget.onmouseenter = () => !this.isSyncing && Object.assign(this.syncWidget.style, {
                    opacity: '1',
                    background: '#f8f9fa',
                    borderColor: '#0066cc',
                    transform: 'translateY(-1px)'
                });
                this.syncWidget.onmouseleave = () => {
                    this.syncWidget.style.opacity = settings.syncWidgetOpacity;
                    this.updateSyncWidget(this.isSyncing ? 'syncing' : 'normal');
                };
            }

            // Update badge based on pending count
            const pendingChanges = this.getPendingChanges();
            const pendingCount = (pendingChanges.operations?.length || 0) + (pendingChanges.notes?.length || 0);
            if (pendingCount > 0) {
                this.syncBadge.style.display = 'inline-block';
                this.syncBadge.textContent = pendingCount;
            } else {
                this.syncBadge.style.display = 'none';
            }

            // Update widget based on state
            const states = {
                normal: ['#fff', '#ddd', '#666', 'none', 'pointer', this.timeUntilNextSync <= 0 ? 'Sync now' : (this.timeUntilNextSync > 60 ? `${Math.floor(this.timeUntilNextSync/60)}m ${this.timeUntilNextSync%60}s` : `${this.timeUntilNextSync}s`)],
                syncing: ['#e3f2fd', '#2196f3', '#1976d2', 'ft-spin 1s linear infinite', 'default', 'Syncing...'],
                success: ['#e8f5e8', '#4caf50', '#2e7d32', 'none', 'pointer', 'Synced!'],
                error: ['#ffebee', '#f44336', '#c62828', 'none', 'pointer', 'Failed'],
                offline: ['#f5f5f5', '#ccc', '#999', 'none', 'default', 'Offline']
            };

            const [bg, border, color, animation, cursor, text] = states[state] || states.normal;
            const [icon, textEl, badge] = this.syncWidget.children;

            Object.assign(this.syncWidget.style, {
                background: bg,
                borderColor: border,
                cursor
            });
            Object.assign(icon.style, {
                animation,
                color
            });
            textEl.textContent = text;
            textEl.style.color = color;

            // Auto-revert success to normal
            if (state === 'success') {
                setTimeout(() => this.updateSyncWidget('normal'), 2000);
            }
        }

        // Only sync when tab is focused to prevent redundant requests form multiple tabs
        handleVisibilityChange() {
            if (document.visibilityState === 'visible') {
                DEBUG && console.log('[FicTracker] Tab is visible – starting sync timer');
                this.startSyncTimer();
            } else {
                DEBUG && console.log('[FicTracker] Tab hidden – stopping sync timer');
                this.stopSyncTimer();
            }
        }

        // Start periodic sync timer
        startSyncTimer() {
            // Stop any existing sync timers to avoid duplicates
            this.stopSyncTimer();

            // If syncing is disabled in settings, update UI and exit
            if (!settings.syncEnabled) {
                DEBUG && console.log('[FicTracker] Sync is disabled, timer not started.');
                this.updateSyncWidget();
                return;
            }

            const now = Date.now();
            const lastSync = parseInt(this.storageManager.getItem(this.LAST_SYNC_KEY)) || 0;
            // Calculate how long it's been since the last successful sync
            const timeSinceLastSync = (now - lastSync);

            DEBUG && console.log(`[FicTracker] Time since last sync: ${timeSinceLastSync / 1000}s`);

            // If enough time has passed, sync immediately and start interval
            if (timeSinceLastSync >= this.syncInterval) {
                DEBUG && console.log('[FicTracker] Sync interval exceeded - performing immediate sync');
                this.timeUntilNextSync = 0;
                this.performSync();
                this.syncTimer = setInterval(() => {
                    if (this.isOnline) this.performSync();
                }, this.syncInterval);

                // If not enough time has passed, schedule a one-time timeout to sync later    
            } else {
                const timeUntilNextSync = this.syncInterval - timeSinceLastSync;
                this.timeUntilNextSync = Math.ceil(timeUntilNextSync / 1000);

                DEBUG && console.log(`[FicTracker] Sync interval not yet reached - scheduling in ${timeUntilNextSync / 1000}s`);

                this.syncTimeout = setTimeout(() => {
                    if (this.isOnline) this.performSync();
                    this.syncTimer = setInterval(() => {
                        if (this.isOnline) this.performSync();
                    }, this.syncInterval);
                    this.syncTimeout = null; // clear reference
                }, timeUntilNextSync);
            }
        }

        // Stop sync timer
        stopSyncTimer() {
            DEBUG && console.log('[FicTracker] Stopping sync timers...');

            // Clear the periodic sync interval if it's active
            if (this.syncTimer) {
                clearInterval(this.syncTimer);
                this.syncTimer = null;
            }

            // Clear any scheduled one-time sync timeout if it's active
            if (this.syncTimeout) {
                clearTimeout(this.syncTimeout);
                this.syncTimeout = null;
            }
        }

        // Handle online event
        handleOnline() {
            this.isOnline = true;
            DEBUG && console.log('[FicTracker] Back online, resuming sync');
            this.performSync();
        }

        // Handle offline event
        handleOffline() {
            this.isOnline = false;
            DEBUG && console.log('[FicTracker] Gone offline, pausing sync');
        }

        // Add a change to the pending queue
        addPendingStatusChange(action, statusKey, fanficId) {
            const pendingChanges = this.getPendingChanges();

            // Optimize operations - remove conflicting operations
            const newOperation = {
                action,
                key: statusKey,
                value: fanficId
            };

            DEBUG && console.log(`[FicTracker] Queuing pending status change: ${action} ${statusKey} → ${fanficId}`);
            this.optimizeOperations(pendingChanges.operations, newOperation);

            pendingChanges.operations.push(newOperation);
            this.savePendingChanges(pendingChanges);
        }

        // Add a note update to the pending queue
        addPendingNoteUpdate(fanficId, text, date) {
            const pendingChanges = this.getPendingChanges();
            DEBUG && console.log(`[FicTracker] Updating note for fanficId="${fanficId}", text="${text}", date="${date}"`);

            // Remove any existing note update for this fanfic
            pendingChanges.notes = pendingChanges.notes.filter(
                update => update.fanficId !== fanficId
            );

            pendingChanges.notes.push({
                fanficId,
                text: text || '',
                date: date || null
            });
            
            this.savePendingChanges(pendingChanges);
        }

        // Optimize operations by removing conflicting ones
        optimizeOperations(operations, newOperation) {
            const {
                action,
                key,
                value
            } = newOperation;

            // Find and remove conflicting operations
            for (let i = operations.length - 1; i >= 0; i--) {
                const existing = operations[i];

                if (existing.key === key && existing.value === value) {
                    // Same key-value pair
                    if (existing.action !== action) {
                        // Conflicting actions (add vs remove) - remove the existing one
                        operations.splice(i, 1);
                        DEBUG && console.log(`[FicTracker] Optimized conflicting operations for ${key}:${value}`);
                    } else {
                        // Same action - remove duplicate
                        DEBUG && console.log(`[FicTracker] Removed duplicate operation for ${key}:${value}`);
                        return; // Don't add the new operation either
                    }
                }
            }
        }

        // Get pending changes from localStorage
        getPendingChanges() {
            try {
                const changes = this.storageManager.getItem(this.PENDING_CHANGES_KEY);
                return changes ? JSON.parse(changes) : {
                    operations: [],
                    notes: []
                };
            } catch (error) {
                DEBUG && console.error('[FicTracker] Error parsing pending changes:', error);
                return {
                    operations: [],
                    notes: []
                };
            }
        }

        // Save pending changes to localStorage
        savePendingChanges(changes) {
            this.storageManager.setItem(this.PENDING_CHANGES_KEY, JSON.stringify(changes));
            DEBUG && console.log('[FicTracker] Saved pending changes to storage');
        }

        // Clear pending changes
        clearPendingChanges() {
            // Reset pending operations and notes to an empty state in storage
            this.storageManager.setItem(this.PENDING_CHANGES_KEY, JSON.stringify({
                operations: [],
                notes: []
            }));
            DEBUG && console.log('[FicTracker] Cleared all pending changes (operations and notes).');
        }

        // Perform sync
        async performSync() {
            if (!this.isOnline) {
                DEBUG && console.log('[FicTracker] Offline, skipping sync');
                return;
            }

            // update widget appropriately
            this.isSyncing = true;
            this.updateSyncWidget('syncing');

            const pendingChanges = this.getPendingChanges();
            DEBUG && console.log('[FicTracker] Performing sync, pending operations:', pendingChanges.operations.length, 'notes:', pendingChanges.notes.length);

            try {
                let syncData = {
                    action: 'sync',
                    queue: pendingChanges
                }

                DEBUG && console.log('[FicTracker] Starting sync:', syncData);

                const response = await this.sendSyncRequest(syncData);

                if (response.success) {
                    // Update local storage with server data
                    this.updateLocalStorage(response.status_data);

                    this.timeUntilNextSync = this.syncInterval / 1000;
                    this.isSyncing = false;
                    this.updateSyncWidget('success');

                    // Update notes if provided
                    if (response.notes) {
                        this.updateLocalNotes(response.notes);
                    }

                    // Clear pending changes
                    this.clearPendingChanges();

                    // Update last sync timestamp
                    this.storageManager.setItem(this.LAST_SYNC_KEY, Date.now().toString());

                    DEBUG && console.log('[FicTracker] Sync completed successfully');
                } else {
                    DEBUG && console.error('[FicTracker] Sync failed:', response.error || 'Unknown error');
                    this.isSyncing = false;
                    this.updateSyncWidget('error');
                }

            } catch (error) {
                this.isSyncing = false;
                this.updateSyncWidget('error');
                DEBUG && console.error('[FicTracker] Sync failed:', error);
            }
        }

        // Send sync request to server
        async sendSyncRequest(data) {
            // Wrap the sync request in a promise to handle async response with resolve/reject
            return new Promise((resolve, reject) => {
                DEBUG && console.log('[FicTracker] Sending sync request to:', settings.sheetUrl);

                // Use GM_xmlhttpRequest instead of fetch to avoid CORS
                GM_xmlhttpRequest({
                    method: 'POST',
                    url: settings.sheetUrl,
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    data: JSON.stringify(data),
                    timeout: 15000, // 15s timeout
                    onload: (response) => {
                        try {
                            const result = JSON.parse(response.responseText);
                            DEBUG && console.log('[FicTracker] Server response received and parsed successfully:', result);
                            resolve(result);
                        } catch (error) {
                            // Reject if server returns non-JSON or fails to parse
                            reject(new Error('Invalid JSON response'));
                        }
                    },
                    onerror: (error) => {
                        DEBUG && console.error('[FicTracker] Sync request failed due to network error:', error);
                        reject(new Error('Network error'));
                    },
                    ontimeout: () => {
                        DEBUG && console.warn('[FicTracker] Sync request timed out.');
                        reject(new Error('Request timeout'));
                    }
                });
            });
        }

        // Update local storage with server data
        updateLocalStorage(serverData) {
            // Iterate through the list of keys that are eligible for syncing
            for (const key of this.syncedKeys) {
                // If the server response contains the key, update local storage with its value
                if (serverData.hasOwnProperty(key)) {
                    this.storageManager.setItem(key, serverData[key]);
                    DEBUG && console.log(`[FicTracker] Synced key "${key}" updated from server data.`);
                }
            }
        }

        // Update local storage with server notes data
        updateLocalNotes(serverNotes) {
            // Overwrite local user notes with the latest version from the server
            this.storageManager.setItem('FT_userNotes', JSON.stringify(serverNotes));
            DEBUG && console.log('[FicTracker] Local user notes updated from server.');
        }

        // Get sync status info
        getSyncStatus() {
            // Retrieve current pending operations and notes from storage
            const pendingChanges = this.getPendingChanges();
            const lastSync = this.storageManager.getItem(this.LAST_SYNC_KEY);

            DEBUG && console.log('[FicTracker] Sync status retrieved:', {
                pendingOperations: pendingChanges.operations.length,
                pendingNoteUpdates: pendingChanges.notes.length,
                lastSync: lastSync ? new Date(parseInt(lastSync)) : null,
                isOnline: this.isOnline
            });


            // Return an object summarizing sync status for UI/debug purposes
            return {
                pendingOperations: pendingChanges.operations.length,
                pendingNoteUpdates: pendingChanges.notes.length,
                lastSync: lastSync ? new Date(parseInt(lastSync)) : null,
                isOnline: this.isOnline
            };
        }
    }


    // Class for bookmark data and tag management abstraction to keep things DRY
    class BookmarkTagManager {
        constructor(htmlSource) {
            // If it's already a document, use it directly, otherwise parse the HTML string
            if (htmlSource instanceof Document) {
                this.doc = htmlSource;
            } else {
                // Use DOMParser to parse the HTML response
                const parser = new DOMParser();
                this.doc = parser.parseFromString(htmlSource, 'text/html');
            }
        }

        // Get the work ID from the DOM
        getWorkId() {
            return this.doc.getElementById('kudo_commentable_id')?.value || null;
        }

        // Get the bookmark ID from the form's action attribute
        getBookmarkId() {
            const bookmarkForm = this.doc.querySelector('div#bookmark_form_placement form');
            return bookmarkForm ? bookmarkForm.getAttribute('action').split('/')[2] : null;
        }

        // Get the pseud ID from the input
        getPseudId() {
            const singlePseud = this.doc.querySelector('input#bookmark_pseud_id');

            if (singlePseud) {
                return singlePseud.value;
            } else {
                // If user has multiple pseuds - use the default one to create bookmark
                const pseudSelect = this.doc.querySelector('select#bookmark_pseud_id');
                return pseudSelect?.value || null;
            }
        }

        // Gather all bookmark-related data into an object
        getBookmarkData() {
            return {
                workId: this.getWorkId(),
                bookmarkId: this.getBookmarkId(),
                pseudId: this.getPseudId(),
                bookmarkTags: this.getBookmarkTags(),
                notes: this.getBookmarkNotes(),
                collections: this.getBookmarkCollections(),
                isPrivate: this.isBookmarkPrivate(),
                isRec: this.isBookmarkRec()
            };
        }

        getBookmarkTags() {
            return this.doc.querySelector('#bookmark_tag_string').value.split(', ').filter(tag => tag.length > 0);;
        }

        getBookmarkNotes() {
            return this.doc.querySelector('textarea#bookmark_notes').textContent;
        }

        getBookmarkCollections() {
            return this.doc.querySelector('#bookmark_collection_names').value.split(',').filter(col => col.length > 0);;
        }

        isBookmarkPrivate() {
            return this.doc.querySelector('#bookmark_private')?.checked || false;
        }

        isBookmarkRec() {
            return this.doc.querySelector('#bookmark_recommendation')?.checked || false;
        }

        async processTagToggle(tag, isTagPresent, bookmarkData, authenticityToken, storageKey, storageManager, requestManager, remoteSyncManager) {
            // Toggle the bookmark tag and log the action
            if (isTagPresent) {
                DEBUG && console.log(`[FicTracker] Removing tag: ${tag}`);
                bookmarkData.bookmarkTags.splice(bookmarkData.bookmarkTags.indexOf(tag), 1);
                storageManager.removeIdFromCategory(storageKey, bookmarkData.workId);
                
            if (remoteSyncManager) {
                remoteSyncManager.addPendingStatusChange('remove', storageKey, bookmarkData.workId);
            }

            } else {
                DEBUG && console.log(`[FicTracker] Adding tag: ${tag}`);
                bookmarkData.bookmarkTags.push(tag);
                storageManager.addIdToCategory(storageKey, bookmarkData.workId);

                if (remoteSyncManager) {
                    remoteSyncManager.addPendingStatusChange('add', storageKey, bookmarkData.workId);
                }
            }


            // If the bookmark exists - update it, if not - create a new one
            if (bookmarkData.workId !== bookmarkData.bookmarkId) {
                // If bookmark becomes empty (no notes, tags, collections) after status change - delete it
                const hasNoData = bookmarkData.notes === "" && bookmarkData.bookmarkTags.length === 0 && bookmarkData.collections.length === 0;

                if (settings.deleteEmptyBookmarks && hasNoData) {
                    DEBUG && console.log(`[FicTracker] Deleting empty bookmark ID: ${bookmarkData.bookmarkId}`);
                    await requestManager.deleteBookmark(bookmarkData.bookmarkId, authenticityToken);
                    bookmarkData.bookmarkId = bookmarkData.workId;
                } else {
                    // Update the existing bookmark
                    await requestManager.updateBookmark(bookmarkData.bookmarkId, authenticityToken, bookmarkData);
                }

            } else {
                // Create a new bookmark
                bookmarkData.isPrivate = settings.newBookmarksPrivate;
                bookmarkData.isRec = settings.newBookmarksRec;
                bookmarkData.bookmarkId = await requestManager.createBookmark(bookmarkData.workId, authenticityToken, bookmarkData);

                DEBUG && console.log(`[FicTracker] Created bookmark ID: ${bookmarkData.bookmarkId}`);
            }

            return bookmarkData
        }
    }


    // Class for managing bookmark status updates
    class BookmarkManager {
        constructor(baseApiUrl) {
            this.requestManager = new RequestManager(baseApiUrl);
            this.storageManager = new StorageManager();
            this.bookmarkTagManager = new BookmarkTagManager(document);

            // Start remote manager if enabled in settings
            if (settings.syncEnabled) {
                this.remoteSyncManager = new RemoteStorageSyncManager();
                this.remoteSyncManager.init();
            }

            // Initialize user notes manager
            this.userNotesManager = new CustomUserNotesManager(this.storageManager, this.remoteSyncManager);


            // Extract bookmark-related data from the DOM
            this.bookmarkData = this.bookmarkTagManager.getBookmarkData();

            DEBUG && console.log(`[FicTracker] Initialized BookmarkManager with data:`);
            DEBUG && console.table(this.bookmarkData)

            // Hide the default "to read" button if specified in settings
            if (settings.hideDefaultToreadBtn) {
                document.querySelector('li.mark').style.display = "none";
            }

            this.addButtons();
        }

        // Add action buttons and notes to the UI
        addButtons() {
            const actionsMenu = document.querySelector('ul.work.navigation.actions');
            const bottomActionsMenu = document.querySelector('div#feedback > ul');
            
            // Add user notes if enabled
            if (settings.displayUserNotes) {
                const ficWrapperContainer = document.querySelector('#main div.wrapper');
                const containerForNotes = ficWrapperContainer.parentElement;

                ficWrapperContainer.insertAdjacentHTML('afterend', 
                    this.userNotesManager.generateNoteHtml(this.bookmarkData.workId, true)
                );
                this.userNotesManager.setupNoteHandlers(containerForNotes, true);
            }

            settings.statuses.forEach(({
                tag,
                positiveLabel,
                negativeLabel,
                selector,
                enabled
            }) => {

                // Skip rendering btn for disabled status
                if (!enabled) return;

                const isTagged = this.bookmarkData.bookmarkTags.includes(tag);
                const buttonHtml = `<li class="mark-as-read" id="${selector}"><a href="#">${isTagged ? negativeLabel : positiveLabel}</a></li>`;

                actionsMenu.insertAdjacentHTML('beforeend', buttonHtml);

                // insert button duplicate at the bottom
                if (settings.displayBottomActionButtons) {
                    bottomActionsMenu.insertAdjacentHTML('beforeend', buttonHtml);
                }
            });

            this.setupClickListeners();
        }

        // Set up click listeners for each action button
        setupClickListeners() {
            settings.statuses.forEach(({
                selector,
                tag,
                positiveLabel,
                negativeLabel,
                storageKey,
                enabled
            }) => {
                // Don't setup listener for disabled btn
                if (!enabled) return;

                // Use querySelectorAll to get all elements with the duplicate ID (bottom menu)
                document.querySelectorAll(`#${selector}`).forEach(button => {
                    button.addEventListener('click', (event) => {
                        event.preventDefault();

                        this.handleActionButton(tag, positiveLabel, negativeLabel, selector, storageKey);
                    });
                });
            });
        }

        // Handle the action for adding/removing/deleting a bookmark tag
        async handleActionButton(tag, positiveLabel, negativeLabel, selector, storageKey) {
            const authenticityToken = this.requestManager.getAuthenticityToken();
            const isTagPresent = this.bookmarkData.bookmarkTags.includes(tag);

            // Consider button bottom menu duplication
            const buttons = document.querySelectorAll(`#${selector} a`);

            // Disable the buttons and show loading state
            buttons.forEach((btn) => {
                btn.innerHTML = settings.loadingLabel;
                btn.disabled = true;
            });

            try {
                // Send tag toggle request and modify cached bookmark data
                this.bookmarkData = await this.bookmarkTagManager.processTagToggle(tag, isTagPresent, this.bookmarkData, authenticityToken,
                    storageKey, this.storageManager, this.requestManager, this.remoteSyncManager);

                // Update the labels for all buttons
                buttons.forEach((btn) => {
                    btn.innerHTML = isTagPresent ? positiveLabel : negativeLabel;
                });

            } catch (error) {
                console.error(`[FicTracker] Error during bookmark operation:`, error);
                buttons.forEach((btn) => {
                    btn.innerHTML = 'Error! Try Again';
                });
            } finally {
                buttons.forEach((btn) => {
                    btn.disabled = false;
                });
            }
        }


    }

    // Class for handling features on works list page
    class WorksListHandler {
        constructor() {
            this.storageManager = new StorageManager();
            this.requestManager = new RequestManager('https://archiveofourown.org/');

            // Start remote manager if enabled in settings
            if (settings.syncEnabled) {
                this.remoteSyncManager = new RemoteStorageSyncManager();
                this.remoteSyncManager.init();
            }

            // Initialize user notes manager
            this.userNotesManager = new CustomUserNotesManager(this.storageManager, this.remoteSyncManager);

            this.loadStoredIds();

            // Update the work list upon initialization
            this.updateWorkList();

            // Listen for clicks on quick tag buttons
            this.setupQuickTagListener();
        }

        // Retrieve stored IDs for different statuses
        loadStoredIds() {
            this.worksStoredIds = settings.statuses.reduce((acc, status) => {
                if (status.enabled) {
                    acc[status.storageKey] = this.storageManager.getIdsFromCategory(status.storageKey);
                }
                return acc;
            }, {});
        }

        // Execute features for each work on the page
        updateWorkList() {
            const works = document.querySelectorAll('li.work.blurb, li.bookmark.blurb');
            works.forEach(work => {
                // Skip deleted works that show the "deleted" message
                if (work.querySelector('.message')?.textContent.includes('has been deleted')) {
                    DEBUG && console.log('[FicTracker] Skipping deleted work:', work.id);
                    return;
                }

                const workId = this.getWorkId(work);
                // Skip if we couldn't get a valid work ID
                if (!workId) {
                    DEBUG && console.log('[FicTracker] Skipping work - could not get work ID');
                    return;
                }

                // Only status highlighting for now, TBA
                this.highlightWorkStatus(work, workId);

                // Reload stored IDs to reflect any changes in storage (from fic card)
                this.loadStoredIds(); 

                this.addQuickTagDropdown(work);

                // Display note management btn if enabled
                if (settings.displayUserNotesBtn) {
                    this.addNoteButton(work);
                }
            });

            // Prefill all notes, listen for edits
            this.prefillNotes();
        }

        // Get the work ID from DOM
        getWorkId(work) {
            const link = work.querySelector('h4.heading a');
            const workId = link.href.split('/').pop();
            return workId;
        }

        // Change the visuals of each work's status
        highlightWorkStatus(work, workId) {
            let shouldBeCollapsable = false;
            const appliedStatuses = new Set();

            // First check localStorage statuses
            Object.entries(this.worksStoredIds).forEach(([status, storedIds]) => {
                const statusClass = `glowing-border-${status}`;
                const hasStatus = storedIds.includes(workId);

                if (hasStatus) {
                    // Add appropriate class for collapsable works
                    work.classList.add(statusClass);
                    appliedStatuses.add(status);

                    const statusSettings = getStatusSettingsByStorageKey(status);
                    if (statusSettings?.collapse === true) {
                        shouldBeCollapsable = true;
                    }
                } else {
                    work.classList.remove(statusClass);
                }
            });

            // If no status was found in localStorage, check for bookmark tags in the card
            if (appliedStatuses.size === 0) {
                const userModule = work.querySelector('div.own.user.module.group');
                DEBUG && console.debug(`[FicTracker] Checking bookmark card for work ${workId}`);
                if (userModule) {
                    const tagsList = userModule.querySelector('ul.meta.tags.commas');
                    if (tagsList) {
                        const tagElements = tagsList.querySelectorAll('a.tag');
                        tagElements.forEach(tagElement => {
                            const tagText = tagElement.textContent.trim();
                            // Find matching status in settings
                            const matchingStatus = settings.statuses.find(status => status.tag === tagText);
                            if (matchingStatus) {
                                const statusClass = `glowing-border-${matchingStatus.storageKey}`;
                                work.classList.add(statusClass);
                                appliedStatuses.add(matchingStatus.storageKey);
                                DEBUG && console.log(`[FicTracker] Found status tag: ${tagText}`);

                                // Add the work ID to storage if it's not there yet
                                this.storageManager.addIdToCategory(matchingStatus.storageKey, workId);
                                DEBUG && console.log(`[FicTracker] Synced work ${workId} to storage for status: ${matchingStatus.storageKey}`);

                                if (matchingStatus.collapse === true) {
                                    shouldBeCollapsable = true;
                                }
                            }
                        });
                    }
                }
            }

            // If at least one of the statuses of the work is set to be collapsable - let it be so
            if (shouldBeCollapsable) {
                work.classList.add('FT_collapsable');
            } else {
                work.classList.remove('FT_collapsable');
            }
        }


        // Add quick tag toggler dropdown to the work
        addQuickTagDropdown(work) {
            const workId = this.getWorkId(work);

            // Generate the dropdown options dynamically based on the status categories
            const dropdownItems = Object.entries(this.worksStoredIds).map(([status, storedIds], index) => {
                let statusSettings = getStatusSettingsByStorageKey(status);
                // Don't render disabled statuses
                if (!statusSettings.enabled) return;

                const statusLabel = statusSettings[storedIds.includes(workId) ? 'negativeLabel' : 'positiveLabel'];
                return `<li><a href="#" class="work_quicktag_btn" data-work-id="${workId}" data-status-tag="${statusSettings.tag}" data-status-name="${status}">${statusLabel}</a></li>`;
            });

            // No status is enabled, dont render Change Status menu
            if (dropdownItems.length === 0) return;

            work.querySelector('dl.stats').insertAdjacentHTML('beforeend', `
                <header id="header" class="region" style="padding: 0; font-size: 1em !important; cursor: pointer; opacity: 1; word-spacing: normal !important; display: inline;">
                <ul class="navigation actions">
                    <li class="dropdown" aria-haspopup="true" style="position: relative !important;>
                        <a href="#" class="dropdown-toggle" data-toggle="dropdown" data-target="#">✨ Change Status ▼</a>
                        <ul class="menu dropdown-menu" style="width: auto !important;">
                            ${dropdownItems.join('')}
                        </ul>
                    </li>
                </ul>
                </header>
            `);
        }

        // Listen for clicks on quicktag dropdown items
        setupQuickTagListener() {
            const worksContainer = document.querySelector('div#main.filtered.region');
            // Event delegation for optimization
            worksContainer.addEventListener('click', async (event) => {
                if (event.target.matches('a.work_quicktag_btn')) {
                    const targetStatusTag = event.target.dataset.statusTag;
                    const workId = event.target.dataset.workId;
                    const storageKey = event.target.dataset.statusName;
                    const statusSettings = getStatusSettingsByStorageKey(storageKey);

                    event.target.innerHTML = settings.loadingLabel;

                    // Get request to retrieve work bookmark data
                    const bookmarkData = await this.getRemoteBookmarkData(event.target);
                    const authenticityToken = this.requestManager.getAuthenticityToken();
                    const tagExists = bookmarkData.bookmarkTags.includes(targetStatusTag);

                    try {
                        // Send tag toggle request and modify cached bookmark data
                        this.bookmarkData = await this.bookmarkTagManager.processTagToggle(targetStatusTag, tagExists, bookmarkData, authenticityToken,
                            storageKey, this.storageManager, this.requestManager, this.remoteSyncManager);

                        // Handle both search page and bookmarks page cases for work retrieval
                        const work = document.querySelector(`li#work_${workId}`) || document.querySelector(`li.work-${workId}`);
                        // Update data from localStorage to properly highlight work
                        this.loadStoredIds();
                        this.highlightWorkStatus(work, workId);
                        event.target.innerHTML = tagExists ?
                            statusSettings.positiveLabel :
                            statusSettings.negativeLabel;
                    } catch (error) {
                        console.error(`[FicTracker] Error during bookmark operation:`, error);
                    }

                }
            })
        }

        // Add note functionality to the work
        addNoteButton(work) {
            const workId = this.getWorkId(work);
            const container = work.querySelector('div.header.module');
            
            // Add the note block
            container.insertAdjacentHTML('beforeend', 
                this.userNotesManager.generateNoteHtml(workId)
            );
        }

        // Setup note handlers for the works list
        prefillNotes() {
            if (!settings.displayUserNotes) return;
            
            const container = document.querySelector('div#main.filtered.region');
            this.userNotesManager.setupNoteHandlers(container);
        }

        // Retrieves bookmark data (if exists) for a given work, by sending HTTP GET req
        async getRemoteBookmarkData(workElem) {
            DEBUG && console.log(`[FicTracker] Quicktag status change, requesting bookmark data workId=${workElem.dataset.workId}`);

            try {
                const data = await this.requestManager.sendRequest(`/works/${workElem.dataset.workId}`, null, null, 'GET');
                DEBUG && console.log('[FicTracker] Bookmark data request successful:');
                DEBUG && console.table(data);

                // Read the response body as text
                const html = await data.text();
                this.bookmarkTagManager = new BookmarkTagManager(html);
                const bookmarkData = this.bookmarkTagManager.getBookmarkData();

                DEBUG && console.log('[FicTracker] HTML parsed successfully:');
                DEBUG && console.table(bookmarkData);

                return bookmarkData;

            } catch (error) {
                DEBUG && console.error('[FicTracker] Error retrieving bookmark data:', error);
            }
        }


    }


    // Class for handling the UI & logic for the script settings panel
    class SettingsPageHandler {
        constructor(settings) {
            this.settings = settings;
            this.init();
            
            if (this.settings.syncEnabled) {
                this.initRemoteSyncManager();
            }

        }

        init() {
            // Inject PetiteVue & insert the UI after
            this.injectVueScript(() => {
                this.loadSettingsPanel();
            });
        }

        initRemoteSyncManager() {
            if (!this.remoteSyncManager) {
                this.remoteSyncManager = new RemoteStorageSyncManager();
                this.remoteSyncManager.init();
            }
        }

        // Adding lightweight Vue.js fork (6kb) via CDN
        // Using it saves a ton of repeated LOC to attach event handlers & data binding
        // PetiteVue Homepage: https://github.com/vuejs/petite-vue
        injectVueScript(callback) {
            const vueScript = document.createElement('script');
            vueScript.src = 'https://unpkg.com/petite-vue';
            document.head.appendChild(vueScript);
            vueScript.onload = callback;
        }

        // Load HTML template for the settings panel from GitHub repo
        // Insert into the AO3 preferences page & attach Vue app
        loadSettingsPanel() {
            const container = document.createElement('fieldset');

            // HTML template for the settings panel
            const settingsPanelHtml = `
                <div v-scope @vue:mounted="onMounted">
                <!-- FicTracker Settings Panel HTML -->
                <h1>FicTracker Settings</h1>
                <section>
                    <label for="status_select">Status to Configure:</label>
                    <select id="status_select" v-model="selectedStatus">
                        <option value="0">Finished</option>
                        <option value="1">Favorite</option>
                        <option value="2">To Read</option>
                        <option value="3">Disliked</option>
                    </select>
                    <details open>
                        <summary>Tag And Labels Settings</summary>
                        <ul id="input_settings">
                            <li>
                                <input type="checkbox" id="toggle_enabled" v-model="currentSettings.enabled">
                                <label for="toggle_enabled">Enabled</label>
                            </li>
                            <li>
                                <input type="checkbox" id="toggle_collapsable" v-model="currentSettings.collapse">
                                <label for="toggle_collapsable" title="If enabled, fanfics with this tag will be collapsed. You can uncollapse them by hovering over.">
                                    Collapse works with this tag
                                </label>
                            </li>
                            <li>
                                <input type="checkbox" id="toggle_hide" v-model="currentSettings.hide">
                                <label for="toggle_hide" title="If enabled, fanfics with this tag will be completely hidden from your view.">
                                    Hide works with this tag
                                </label>
                            </li>
                            <li>
                                <input type="checkbox" id="toggle_displayInDropdown" v-model="currentSettings.displayInDropdown">
                                <label for="toggle_displayInDropdown" title="If enabled, this tag will appear in the top right dropdown.">
                                    Display this tag in dropdown
                                </label>
                            </li>
                            <li>
                                <label for="tag_name">Tag Name:</label>
                                <input type="text" id="tag_name" v-model="currentSettings.tag">
                            </li>
                            <li>
                                <label for="dropdown_label">Dropdown Label:</label>
                                <input type="text" id="dropdown_label" v-model="currentSettings.dropdownLabel">
                            </li>
                            <li>
                                <label for="positive_label">Action Label:</label>
                                <input type="text" id="positive_label" v-model="currentSettings.positiveLabel">
                            </li>
                            <li>
                                <label for="negative_label">Remove Action Label:</label>
                                <input type="text" id="negative_label" v-model="currentSettings.negativeLabel">
                            </li>
                        </ul>
                    </details>
                </section>
                <section>
                    <details id="highlighting_settings">
                        <summary>Highlighting Settings</summary>
                        <ul>
                            <li>
                                <label for="highlight_color">Highlight Color:</label>
                                <input type="color" id="highlight_color" v-model="currentSettings.highlightColor">
                            </li>
                            <li>
                                <label for="border_size">Border Size:</label>
                                <input type="range" id="border_size" min="0" max="20" v-model="currentSettings.borderSize">
                            </li>
                            <li>
                                <label for="highlight_opacity">Opacity:</label>
                                <input type="range" id="highlight_opacity" min="0" max="1" step="0.1" v-model="currentSettings.opacity">
                            </li>
                            <li>
                                <strong>Preview:</strong>
                                <div :style="previewStyle" id="highlighting_preview">
                                    This is a preview box
                                </div>
                            </li>
                        </ul>
                    </details>
                </section>
                <br>
                <section>
                    <!-- Additional Settings -->
                    <h4 class="heading">Additional Settings</h4>
                    <ul>
                        <!-- Core Functionality -->
                        <li>
                            <input type="checkbox" id="toggle_displayUserNotesBtn" v-model="ficTrackerSettings.displayUserNotesBtn">
                            <label for="toggle_displayUserNotesBtn" title="Shows the 📓 note button on each work card for writing personal notes">Display note management button</label>
                        </li>
                        <li>
                            <input type="checkbox" id="toggle_displayUserNotes" v-model="ficTrackerSettings.displayUserNotes">
                            <label for="toggle_displayUserNotes" title="Shows your saved notes directly in work cards as collapsible sections">Display your notes in work cards</label>
                        </li>
                        <li>
                            <input type="checkbox" id="toggle_expandUserNoteDetails" v-model="ficTrackerSettings.expandUserNoteDetails">
                            <label for="toggle_expandUserNoteDetails" title="If enabled, your saved notes will appear expanded in work cards by default. You can still collapse them manually.">
                                Auto-expand your notes in work cards
                            </label>
                        </li>
                        
                        <!-- Bookmark Behavior -->
                        <li>
                            <input type="checkbox" id="toggle_private" v-model="ficTrackerSettings.newBookmarksPrivate">
                            <label for="toggle_private" title="All new bookmarks will be marked as private by default">New bookmarks private by default</label>
                        </li>
                        <li>
                            <input type="checkbox" id="toggle_rec" v-model="ficTrackerSettings.newBookmarksRec">
                            <label for="toggle_rec" title="All new bookmarks will be marked as recommendations by default">New bookmarks marked as rec by default</label>
                        </li>
                        <li>
                            <input type="checkbox" id="toggle_deleteEmptyBookmarks" v-model="ficTrackerSettings.deleteEmptyBookmarks">
                            <label for="toggle_deleteEmptyBookmarks" title="Automatically deletes bookmarks that have no notes, tags, or collections when removing status. Only completely empty bookmarks will be removed.">
                            Auto-delete empty bookmarks
                            </label>
                        </li>
                        
                        <!-- Interface Customization -->
                        <li>
                            <input type="checkbox" id="hide_default_toread" v-model="ficTrackerSettings.hideDefaultToreadBtn">
                            <label for="hide_default_toread" title="Hides AO3's default 'Mark For Later' button to reduce clutter">Hide default Mark For Later button</label>
                        </li>
                        <li>
                            <input type="checkbox" id="toggle_displayBottomActionButtons" v-model="ficTrackerSettings.displayBottomActionButtons">
                            <label for="toggle_displayBottomActionButtons" title="Adds duplicate tracking buttons at the bottom of long work lists for easier access">Duplicate action buttons at page bottom</label>
                        </li>
                        
                        <!-- Advanced Options -->
                        <li>
                            <input type="checkbox" id="toggle_debug" v-model="ficTrackerSettings.debug">
                            <label for="toggle_debug" title="Enables console logging and debug information for troubleshooting">Debug mode (for troubleshooting)</label>
                        </li>
                        
                        <!-- Reset Option -->
                        <li style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #ccc;">
                            <input type="submit" id="reset_settings" value="Reset Settings to Default"
                            title="Reset all FicTracker settings to their original default values"
                            @click="resetSettings">
                        </li>
                    </ul>
                </section>
                <br>
                <section>
                <!-- Automatic Google Sheet Sync -->
                    <h4 class="heading">
                        Google Sheet Storage
                        <a href="https://greasyfork.org/en/scripts/513435-ao3-fictracker" target="_blank" style="font-size: 0.8em; margin-left: 10px;">[Setup Guide]</a>
                        <a href="#" @click.prevent="displayModal('What is Google Sheets Storage Sync', modalGoogleSyncInfo)" style="font-size: 0.8em; margin-left: 5px;">[What's this?]</a>
                    </h4>
                    <ul>
                        <li>
                            <label>
                                <input type="checkbox" v-model="ficTrackerSettings.syncEnabled"> 
                                Enable automatic sync
                            </label>
                        </li>
                        <div v-show="ficTrackerSettings.syncEnabled">
                            <li>
                                <label title="Show a floating sync status indicator with countdown timer and manual sync button">
                                    <input type="checkbox" v-model="ficTrackerSettings.syncWidgetEnabled"> 
                                    Show sync status widget
                                </label>
                            </li>
                            <li v-if="ficTrackerSettings.syncWidgetEnabled">
                                <label for="sync_widget_opacity">Sync widget opacity:</label>
                                <input type="range" id="sync_widget_opacity" 
                                    v-model="ficTrackerSettings.syncWidgetOpacity" 
                                    min="0.1" max="1" step="0.1"
                                    style="width: 200px; margin-right: 10px;">
                                <strong>{{ ficTrackerSettings.syncWidgetOpacity }}</strong>
                            </li>
                            <li>
                                <label for="sheet_url">Google Script URL:</label>
                                <input type="text" id="sheet_url" v-model="ficTrackerSettings.sheetUrl" :disabled="ficTrackerSettings.syncDBInitialized" placeholder="https://script.google.com/macros/s/AKfyc.../exec">
                            </li>
                            <li>
                                <label for="sync_interval">Sync interval:</label>
                                <input type="range" id="sync_interval" 
                                    v-model="ficTrackerSettings.syncInterval" 
                                    min="60" max="3600" step="60"
                                    style="width: 200px; margin-right: 10px;">
                                <strong>{{ ficTrackerSettings.syncInterval }} seconds</strong>
                            </li>
                            <li v-if="lastSyncTime && ficTrackerSettings.syncDBInitialized">
                                <strong><label>Last sync:</label>
                                <span>{{ lastSyncTimeFormatted }}</span>
                                <br>
                                <span>
                                    Next sync in {{ timeUntilSync }}s</strong>
                                </span>
                            </li>

                            <!-- Status display with loading states -->
                            <li>
                                <div v-if="loadingStates.testConnection" style="color: #0066cc;">
                                    🔄 Testing connection...
                                </div>
                                <div v-else-if="loadingStates.sync" style="color: #0066cc;">
                                    🔄 Syncing data...
                                </div>
                                <div v-else-if="loadingStates.initialize" style="color: #0066cc;">
                                    🔄 Initializing Google Sheet...
                                </div>
                                <div v-else-if="Object.keys(sheetConnectionStatus).length > 0" :style="{color: sheetConnectionStatus.success ? 'green' : 'red'}">
                                    {{ sheetConnectionStatus.success ? '✅' : '❌' }} {{ sheetConnectionStatus.message }}
                                </div>
                                <div v-else-if="Object.keys(syncFeedback).length > 0" :style="{color: syncFeedback.success ? 'green' : 'red'}">
                                    {{ syncFeedback.success ? '✅' : '❌' }} {{ syncFeedback.message }}
                                </div>
                            </li>

                            <li>
                                <input type="submit" 
                                    @click="testSheetConnection" 
                                    :value="loadingStates.testConnection ? 'Testing...' : 'Test Connection'"
                                    :disabled="loadingStates.testConnection || loadingStates.sync || loadingStates.initialize">
                                
                                <input v-if="ficTrackerSettings.syncDBInitialized" 
                                    type="submit" 
                                    @click="syncNow" 
                                    :value="loadingStates.sync ? 'Syncing...' : 'Sync Now'"
                                    :disabled="loadingStates.testConnection || loadingStates.sync || loadingStates.initialize">
                                
                                <input type="submit" 
                                    v-if="ficTrackerSettings.syncDBInitialized" 
                                    @click="resetSyncSettings" 
                                    value="Reset Sync Settings"
                                    :disabled="loadingStates.testConnection || loadingStates.sync || loadingStates.initialize">

                                <li v-if="readyToInitDB && !ficTrackerSettings.syncDBInitialized">
                                    <input type="submit" 
                                        @click="initializeSheetStorage" 
                                        :value="loadingStates.initialize ? 'Initializing...' : 'Initialize Google Sheet Storage'"
                                        :disabled="loadingStates.testConnection || loadingStates.sync || loadingStates.initialize">
                                </li>
                            </li>
                        </div>
                    </ul>
                </section>
                <br>
                <section>
                    <!-- Manual Import/Export -->
                    <h4 class="heading">Manual Data Import/Export</h4>
                    <ul>
                        <li>
                            Last data export: {{ ficTrackerSettings.lastExportTimestamp }}
                        </li>
                        <li>
                            <div style="display: flex; align-items: center; margin-bottom: 10px;">
                                <input type="checkbox" id="export_status_config" v-model="ficTrackerSettings.exportStatusesConfig" style="margin-right: 8px;">
                                <label for="export_status_config" title="When enabled, exports your customized status settings (colors, labels, tags) along with your data. Useful when setting up FicTracker on another device or sharing your configuration with others. Disable if you only want to export your lists without configuration.">
                                    Export status configuration
                                </label>
                            </div>
                        </li>
                        <li>
                            <div style="display: flex;column-gap: 20px;">
                            <!-- Hidden file input -->
                            <input type="file" id="import_file" accept=".json" style="display: none;" @change="importData">
                            <input type="submit" id="import_data" value="Import data from file..."
                                title="Load your bookmarks data from a local file"
                                @click="document.getElementById('import_file').click(); return false;">
                            <input type="submit" id="export_data" value="Export data to file..."
                                title="Export your bookmarks data to a local file" @click='exportData'>
                            </div>
                        </li>
                    </ul>
                </section>
                <section>
                    <!-- Save Settings -->
                    <div style="text-align: right;">
                        <input type="submit" id="save_settings" value="Save Settings" @click="saveSettings">
                    </div>
                </section>
                </div>
            `
            // Fetching the HTML for settings panel, outsourced for less clutter
            container.innerHTML = settingsPanelHtml;

            document.querySelector('#main').appendChild(container);

            // Initialize the Vue app instance
            PetiteVue.createApp({
                selectedStatus: 0,
                ficTrackerSettings: this.settings,
                lastSyncTime: null,
                timeUntilSync: null,
                sheetConnectionStatus: {},
                syncFeedback: {},
                initStatus: null,
                readyToInitDB: false,
                modalGoogleSyncInfo: "<h2>What is Google Sheets Storage Sync?</h2><p>This feature allows you to sync all your FicTracker data across multiple devices by using Google Sheets as the <b>source of truth</b> data storage. When you first initialize the database on a device, the storage fills with your current data.</p><p><b>Recommendation:</b> If you only use FicTracker on one device, basic syncing via AO3 storage is sufficient. However, if you use multiple devices and want near real-time syncing (~60 seconds), connecting to Google Sheets is worth it. The setup takes only 2-3 minutes.</p><h3>How to connect two devices:</h3><ol><li><b>Master device:</b> Initialize the database by clicking <i>Initialize DB</i> to create your Google Sheets storage.</li><li><b>Second device:</b> Use the same Google Sheets link and click <i>Initialize DB</i>. It will detect the storage is already initialized and sync your data.</li></ol><p>After setup, syncing happens automatically and quickly, keeping your data up-to-date on all devices.</p><h3>What is synced automatically? (Without Google Sheets connection)</h3><ul><li>Bookmarked fics with appropriate tags</li><li>Bookmark notes - these are stored directly on AO3 servers</li></ul><p>Due to technical limitations, <b>fic highlighting</b> and <b>custom user notes</b> cannot be saved on AO3 and require external storage. Google Sheets provides a free, simple, and reliable way to store and sync this data across devices.</p><h3>What requires Google Sheets DB connection?</h3><ul><li>Highlighting sync</li><li>User notes sync</li></ul>",

                // Loading states for different sync feature ops
                loadingStates: {
                    testConnection: false,
                    sync: false,
                    initialize: false
                },

                // Computed 
                get currentSettings() {
                    return this.ficTrackerSettings.statuses[this.selectedStatus];
                },

                get previewStyle() {
                    const s = this.currentSettings;
                    const borderSize = s.borderSize ?? 0;
                    const hasBorder = borderSize > 0;

                    return {
                        height: '50px',
                        border: hasBorder ? `${s.borderSize}px solid ${s.highlightColor}` : 'none',
                        boxShadow: hasBorder ?
                            `0 0 10px ${s.highlightColor}, 0 0 20px ${s.highlightColor}` :
                            'none',
                        opacity: s.opacity
                    };
                },

                get lastSyncTimeFormatted() {
                    if (!this.lastSyncTime) return 'Never';

                    const ts = parseInt(this.lastSyncTime);
                    const date = isNaN(ts) ? null : new Date(ts);

                    return date ? date.toLocaleString() : 'Never';
                },

                // Core Methods
                exportData: this.exportSettings.bind(this),
                importData: this.importSettings.bind(this),
                initRemoteSyncManager: this.initRemoteSyncManager.bind(this),

                // Conditionally add sync method only if remote sync manager is initialized
                performSync: async () => {
                    if (this.remoteSyncManager) {
                        return await this.remoteSyncManager.performSync();
                    } else {
                        console.warn('Sync is not available - sync manager not initialized');
                        throw new Error('Sync is not available');
                    }
                },

                // Pass func through global scope
                displayModal: displayModal,

                saveSettings() {
                    localStorage.setItem('FT_settings', JSON.stringify(this.ficTrackerSettings));
                    alert('Settings successfully saved :)')
                    DEBUG && console.log('[FicTracker] Settings saved.');
                },

                resetSettings() {
                    const confirmed = confirm("Are you sure you want to reset all settings to default? This will delete all saved settings.");
                    if (confirmed) {
                        localStorage.removeItem('FT_settings');
                        alert("Settings have been reset to default.");
                    }
                },

                // Reset all seting related to cloud data sync
                resetSyncSettings() {
                    const confirmed = window.confirm(
                        "This will disable the current database connection.\n\n" +
                        "You can still connect again later using a different link.\n\n" +
                        "Do you want to proceed?"
                    );

                    if (!confirmed) return;

                    this.ficTrackerSettings.sheetUrl = '';
                    this.ficTrackerSettings.syncDBInitialized = false;
                    this.ficTrackerSettings.syncEnabled = false;
                    localStorage.removeItem('FT_lastSync');
                    this.saveSettings();

                    // Clear any existing status messages
                    this.sheetConnectionStatus = {};
                    this.syncFeedback = {};
                },

                // New: Google Sheet Sync logic
                async syncNow() {
                    DEBUG && console.log('[FicTracker] Manual sync initiated...');

                    // Indicate that a sync operation is in progress (for UI/loading indicators)
                    this.loadingStates.sync = true;
                    // Clear previous sync feedback and connection status indicators
                    this.syncFeedback = {};
                    this.sheetConnectionStatus = {};

                    try {
                        // Attempt to perform the sync and update the last successful sync timestamp
                        await this.performSync();
                        this.updateLastSyncTime();

                        // Set success feedback message to inform the user
                        this.syncFeedback = {
                            success: true,
                            message: 'Sync completed successfully!'
                        };

                        // Auto-clear success message after 5 seconds
                        setTimeout(() => {
                            if (this.syncFeedback && this.syncFeedback.success) {
                                this.syncFeedback = {};
                            }
                        }, 5000);

                        // Handle and log sync errors, provide user-facing error message
                    } catch (error) {
                        DEBUG && console.error('[FicTracker] Sync failed:', error);
                        this.syncFeedback = {
                            success: false,
                            message: `Sync failed: ${error.message}`
                        };

                        // Ensure loading state is reset whether sync succeeds or fails
                    } finally {
                        this.loadingStates.sync = false;
                    }
                },

                updateLastSyncTime() {
                    // Retrieve the last sync timestamp from local storage and update internal state
                    const ts = localStorage.getItem('FT_lastSync');
                    this.lastSyncTime = ts;
                },

                // Tests connectivity to the provided Google Sheets URL by sending a ping request.
                // Updates the UI with the result and saves settings if successful.
                testSheetConnection() {
                    const url = this.ficTrackerSettings.sheetUrl;
                    DEBUG && console.log('[FicTracker] Testing connection to Google Sheets URL:', url);

                    // Validate if the Google Sheets URL is provided
                    if (!url) {
                        // Indicate that a test connection is in progress and reset status messages
                        this.sheetConnectionStatus = {
                            success: false,
                            message: 'URL is empty'
                        };
                        return;
                    }

                    this.loadingStates.testConnection = true;
                    this.sheetConnectionStatus = {};
                    this.syncFeedback = {};

                    // Send a ping request to the Google Sheets endpoint to verify connection
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: `${url}?action=ping`,
                        onload: (response) => {
                            this.loadingStates.testConnection = false;

                            try {
                                // Parse the response and update connection status based on server reply
                                const data = JSON.parse(response.responseText);
                                if (data.status === 'success') {
                                    DEBUG && console.log('[FicTracker] Sheet connection successful:', data);

                                    // If connection is successful, save settings and display a confirmation message
                                    this.sheetConnectionStatus = {
                                        success: true,
                                        message: data.data || 'Connection successful!'
                                    };

                                    this.readyToInitDB = true;
                                    this.saveSettings();

                                    // Auto-clear success message after 5 seconds
                                    setTimeout(() => {
                                        if (this.sheetConnectionStatus && this.sheetConnectionStatus.success) {
                                            this.sheetConnectionStatus = {};
                                        }
                                    }, 5000);

                                } else {
                                    DEBUG && console.warn('[FicTracker] Sheet connection failed:', data);

                                    // Handle and display error message if the server returned a failure status
                                    this.sheetConnectionStatus = {
                                        success: false,
                                        message: data.message || 'Unknown error'
                                    };
                                }

                                // Catch JSON parsing errors and report invalid server response
                            } catch (e) {
                                DEBUG && console.error('[FicTracker] Failed to parse server response during test connection:', response.responseText);

                                this.sheetConnectionStatus = {
                                    success: false,
                                    message: 'Invalid response from server'
                                };
                            }
                        },
                        // Handle connection-level errors like CORS or unreachable URL
                        onerror: (err) => {
                            DEBUG && console.error('[FicTracker] Network error during sheet connection test:', err);

                            this.loadingStates.testConnection = false;
                            this.sheetConnectionStatus = {
                                success: false,
                                message: 'Network error - check your connection'
                            };
                        }
                    });
                },

                // Initializes Google Sheets storage by uploading current local FicTracker data.
                // Marks DB as initialized and updates sync timestamp on success.
                initializeSheetStorage() {
                    const url = this.ficTrackerSettings.sheetUrl;
                    // Validate that the Google Sheets URL is set
                    if (!url) {
                        this.sheetConnectionStatus = {
                            success: false,
                            message: 'URL is empty'
                        };
                        return;
                    }

                    // Set loading state and clear any previous status or feedback
                    this.loadingStates.initialize = true;
                    this.sheetConnectionStatus = {};
                    this.syncFeedback = {};

                    // Gather current local storage data to be uploaded to Google Sheets
                    const initData = {
                        FT_userNotes: JSON.stringify(JSON.parse(localStorage.getItem('FT_userNotes') || '{}')),
                        FT_favorites: localStorage.getItem('FT_favorites') || '',
                        FT_disliked: localStorage.getItem('FT_disliked') || '',
                        FT_toread: localStorage.getItem('FT_toread') || '',
                        FT_finished: localStorage.getItem('FT_finished') || '',
                    };

                    DEBUG && console.log('[FicTracker] Initializing Google Sheets with data:', initData);

                    // Send initialization request to Google Sheets endpoint
                    GM_xmlhttpRequest({
                        method: 'POST',
                        url: url,
                        headers: {
                            'Content-Type': 'application/json'
                        },
                        data: JSON.stringify({
                            action: 'initialize',
                            initData
                        }),
                        onload: (response) => {
                            this.loadingStates.initialize = false;

                            try {
                                // Parse and handle successful initialization response
                                const data = JSON.parse(response.responseText);
                                DEBUG && console.log('[FicTracker] DB Initialization response data:', data);

                                if (data.status === 'success') {
                                    // Store sync initialization status, timestamp, and update UI
                                    this.sheetConnectionStatus = {
                                        success: true,
                                        message: data.data?.message || 'Google Sheet initialized successfully!'
                                    };
                                    this.ficTrackerSettings.syncDBInitialized = true;

                                    if (this.ficTrackerSettings.syncEnabled && !this.remoteSyncManager) {
                                        this.initRemoteSyncManager();
                                    }

                                    localStorage.setItem('FT_lastSync', Date.now().toString());
                                    this.saveSettings();
                                    this.updateLastSyncTime();

                                    // Auto-clear success message after 7 seconds
                                    setTimeout(() => {
                                        if (this.sheetConnectionStatus && this.sheetConnectionStatus.success) {
                                            this.sheetConnectionStatus = {};
                                        }
                                    }, 7000);

                                } else {
                                    // Handle error response from server
                                    this.sheetConnectionStatus = {
                                        success: false,
                                        message: data.message || 'Initialization failed'
                                    };
                                }
                                // Catch JSON parsing errors and log them
                            } catch (e) {
                                DEBUG && console.error('[FicTracker] Invalid JSON response during initialization:', response.responseText);

                                this.sheetConnectionStatus = {
                                    success: false,
                                    message: 'Invalid response from server'
                                };
                            }
                        },
                        // Handle connection errors like timeouts or offline state
                        onerror: (err) => {
                            DEBUG && console.error('[FicTracker] Network error during initialization:', err);

                            this.loadingStates.initialize = false;
                            this.sheetConnectionStatus = {
                                success: false,
                                message: 'Network error - check your connection'
                            };
                        }
                    });
                },

                // Lifecycle hook that sets up real-time countdown for next sync based on last sync timestamp.
                onMounted() {
                    // Function to calculate and update time remaining until next sync
                    const trackSyncTime = () => {
                        this.updateLastSyncTime();

                        const elapsed = Date.now() - parseInt(this.lastSyncTime);
                        const remaining = this.ficTrackerSettings.syncInterval - elapsed / 1000;
                        this.timeUntilSync = Math.max(0, Math.round(remaining));
                    }

                    // Initial update on component mount
                    trackSyncTime();
                    // Update the countdown every second
                    setInterval(() => {
                        trackSyncTime();
                    }, 1000);
                }

            }).mount();

        }

        // Exports user data (favorites, finished, toread, disliked, notes, and statuses config) into a JSON file
        exportSettings() {
            // Formatted timestamp for export
            const exportTimestamp = new Date().toISOString().slice(0, 16).replace('T', ' ');
            const exportData = {
                FT_favorites: localStorage.getItem('FT_favorites'),
                FT_finished: localStorage.getItem('FT_finished'),
                FT_toread: localStorage.getItem('FT_toread'),
                FT_disliked: localStorage.getItem('FT_disliked'),
                FT_userNotes: localStorage.getItem('FT_userNotes'),
            };

            // Only include status configuration if the setting is enabled
            if (this.settings.exportStatusesConfig) {
                exportData.FT_statusesConfig = JSON.stringify(this.settings.statuses);
            }

            // Create a Blob object from the export data, converting it to JSON format
            const blob = new Blob([JSON.stringify(exportData, null, 2)], {
            type: 'application/json'
            });

            // Generate a URL for the Blob object to enable downloading
            const url = URL.createObjectURL(blob);

            // Create a temp link to download the generated file data
            const a = document.createElement('a');
            a.href = url;
            a.download = `fictracker_export_${exportTimestamp}.json`;
            document.body.appendChild(a);

            // Trigger a click on the link to initiate the download
            a.click();

            // Cleanup after the download
            document.body.removeChild(a);
            URL.revokeObjectURL(url);

            // Update the last export timestamp
            this.settings.lastExportTimestamp = exportTimestamp;
            localStorage.setItem('FT_settings', JSON.stringify(this.settings));
            DEBUG && console.log('[FicTracker] Data exported at:', exportTimestamp);
        }

        // Imports user data (favorites, finished, toread) from a JSON file
        // Existing storage data is not removed, only new items from file are appended
        importSettings(event) {
            const file = event.target.files[0];
            if (!file) return;

            const reader = new FileReader();
            reader.onload = (e) => {
                try {
                    const importedData = JSON.parse(e.target.result);
                    this.mergeImportedData(importedData);
                    // Reset the file input to allow reimporting the same file
                    event.target.value = '';
                } catch (err) {
                    DEBUG && console.error('[FicTracker] Error importing data:', err);
                    alert('Error importing data. Please check if the file is valid.');
                }
            };
            reader.onerror = () => {
                DEBUG && console.error('[FicTracker] Error reading file:', reader.error);
                alert('Error reading file. Please try again.');
                event.target.value = '';
            };
            reader.readAsText(file);
        }

        mergeImportedData(importedData) {
            // Order: [favorites, finished, toread, disliked, notes]
            let newEntries = [0, 0, 0, 0, 0];

            // Handle comma-separated list data (favorites, finished, toread, disliked)
            const listKeys = ['FT_favorites', 'FT_finished', 'FT_toread', 'FT_disliked'];
            listKeys.forEach((key, index) => {
                if (!importedData[key]) return;
                const currentData = localStorage.getItem(key) ? localStorage.getItem(key).split(',') : [];
                const newData = importedData[key].split(',') || [];

                const initialLen = currentData.length;
                const mergedData = [...new Set([...currentData, ...newData])];

                newEntries[index] = mergedData.length - initialLen;
                localStorage.setItem(key, mergedData.join(','));
            });

            // Handle user notes (JSON data)
            if (importedData.FT_userNotes) {
                try {
                    const currentNotes = JSON.parse(localStorage.getItem('FT_userNotes') || '{}');
                    const importedNotes = JSON.parse(importedData.FT_userNotes);
                    
                    // Merge notes, keeping newer versions if there are conflicts
                    const mergedNotes = { ...currentNotes, ...importedNotes };
                    localStorage.setItem('FT_userNotes', JSON.stringify(mergedNotes));
                    
                    const newNotesCount = Object.keys(importedNotes).length - Object.keys(currentNotes).length;
                    newEntries[4] = Math.max(0, newNotesCount);
                } catch (err) {
                    DEBUG && console.error('[FicTracker] Error merging user notes:', err);
                    newEntries[4] = 0;
                }
            }

            // Handle status configuration
            if (importedData.FT_statusesConfig) {
                try {
                    const importedStatuses = JSON.parse(importedData.FT_statusesConfig);
                    this.settings.statuses = importedStatuses;
                    localStorage.setItem('FT_settings', JSON.stringify(this.settings));
                } catch (err) {
                    DEBUG && console.error('[FicTracker] Error importing status configuration:', err);
                }
            }

            alert(`Data imported successfully!\nNew favorite entries: ${newEntries[0]}\nNew finished entries: ${newEntries[1]}\n` +
                  `New To-Read entries: ${newEntries[2]}\nNew disliked entries: ${newEntries[3]}\nNew notes entries: ${newEntries[4]}`);
            DEBUG && console.log('[FicTracker] Data imported successfully. Stats:', newEntries);
        }

    }

    // Class for managing URL patterns and executing corresponding handlers based on the current path
    class URLHandler {
        constructor() {
            this.handlers = [];
        }

        // Add a new handler with associated patterns to the handlers array
        addHandler(patterns, handler) {
            this.handlers.push({
                patterns,
                handler
            });
        }

        // Iterate through registered handlers to find a match for the current path
        matchAndHandle(currentPath) {
            for (const {
                    patterns,
                    handler
                }
                of this.handlers) {
                if (patterns.some(pattern => pattern.test(currentPath))) {
                    // Execute the corresponding handler if a match is found
                    handler();

                    DEBUG && console.log('[FicTracker] Matched pattern for path:', currentPath);
                    return true;
                }
            }
            DEBUG && console.log('[FicTracker] Unrecognized page', currentPath);
            return false;
        }
    }

    // Main controller that integrates all components of the AO3 FicTracker
    class FicTracker {
        constructor() {

            // Merge stored settings to match updated structure, assign default  settings on fresh installation
            this.mergeSettings();

            // Load settings and initialize other features
            this.settings = this.loadSettings();

            // Filter out disabled statuses
            // this.settings.statuses = this.settings.statuses.filter(status => status.enabled !== false);

            this.initStyles();
            this.addDropdownOptions();
            this.setupURLHandlers();
        }

        // Method to merge settings / store the default ones
        mergeSettings() {
            // Check if settings already exist in localStorage
            let storedSettings = JSON.parse(localStorage.getItem('FT_settings'));

            if (!storedSettings) {
                // No settings found, save default settings
                localStorage.setItem('FT_settings', JSON.stringify(settings));
                console.log('[FicTracker] Default settings have been stored.');
            } else {
                // Check if the version matches the current version from Tampermonkey metadata
                const currentVersion = GM_info.script.version;
                if (!storedSettings.version || storedSettings.version !== currentVersion) {
                    // If versions don't match, merge and update the version
                    storedSettings = _.defaultsDeep(storedSettings, settings);

                    // Update the version marker
                    storedSettings.version = currentVersion;

                    // Save the updated settings back to localStorage
                    localStorage.setItem('FT_settings', JSON.stringify(storedSettings));
                    console.log('[FicTracker] Settings have been merged and updated to the latest version.');
                } else {
                    console.log('[FicTracker] Settings are up to date, no merge needed.');
                }
            }
        }

        // Load settings from the storage or fallback to default ones
        loadSettings() {
            // Measure performance of loading settings from localStorage
            const startTime = performance.now();
            let savedSettings = localStorage.getItem('FT_settings');

            if (savedSettings) {
                try {
                    settings = JSON.parse(savedSettings);
                    DEBUG = settings.debug;
                    DEBUG && console.log(`[FicTracker] Settings loaded successfully:`, savedSettings);
                } catch (error) {
                    DEBUG && console.error(`[FicTracker] Error parsing settings: ${error}`);
                }
            } else {
                DEBUG && console.warn(`[FicTracker] No saved settings found, using default settings.`);
            }

            const endTime = performance.now();
            DEBUG && console.log(`[FicTracker] Settings loaded in ${endTime - startTime} ms`);
            return settings;
        }

        // Initialize custom styles based on loaded settings
        initStyles() {
            // Dynamic styles generation for each status, this will allow adding custom statuses in the future updates
            const statusStyles = StyleManager.generateStatusStyles();

            StyleManager.addCustomStyles(`
                ${statusStyles}

                li.FT_collapsable .landmark,
                li.FT_collapsable .tags,
                li.FT_collapsable .series,
                li.FT_collapsable h5.fandoms.heading,
                li.FT_collapsable .userstuff {
                    display: none;
                }

                /* Uncollapse on hover */
                li.FT_collapsable:hover .landmark,
                li.FT_collapsable:hover .tags,
                li.FT_collapsable:hover ul.series,
                li.FT_collapsable:hover h5.fandoms.heading,
                li.FT_collapsable:hover .userstuff {
                    display: block;
                }

        `);
        }

        // Add new dropdown options for each status to the user menu
        addDropdownOptions() {
            const userMenu = document.querySelector('ul.menu.dropdown-menu');
            const username = userMenu?.previousElementSibling?.getAttribute('href')?.split('/').pop() ?? '';

            if (username) {
                // Loop through each status and add corresponding dropdown options
                this.settings.statuses.forEach((status) => {
                    if (status.displayInDropdown) {
                        userMenu.insertAdjacentHTML(
                            'beforeend',
                            `<li><a href="https://archiveofourown.org/bookmarks?bookmark_search%5Bother_bookmark_tag_names%5D=${status.tag}&user_id=${username}">${status.dropdownLabel}</a></li>`
                        );
                    }
                });
            } else {
                DEBUG && console.warn('[FicTracker] Cannot parse the username!');
            }

            DEBUG && console.log('[FicTracker] Successfully added dropdown options!');
        }

        // Setup URL handlers for different pages
        setupURLHandlers() {
            const urlHandler = new URLHandler();

            // Handler for fanfic pages (chapters, entire work, one shot)
            urlHandler.addHandler(
                [/\/works\/.*(?:chapters|view_full_work)/, /works\/\d+(#\w+-?\w*)?$/],
                () => {
                    const bookmarkManager = new BookmarkManager("https://archiveofourown.org/");
                }
            );

            // Handler for fanfics search/tag list pages & other pages that include a list of fics
            urlHandler.addHandler([
                    /\/works\/search/,
                    /\/works\?.*/,
                    /\/bookmarks$/,
                    /\/users\/bookmarks/,
                    /\/users\/.*\/works/,
                    /\/bookmarks\?page=/,
                    /\/bookmarks\?bookmark_search/,
                    /\/bookmarks\?commit=Sort\+and\+Filter&bookmark_search/,
                    /\/series\/.+/,
                    /\/collections\/.+/,
                    /\/works\?commit=Sort/,
                    /\/works\?work_search/,
                    /\/tags\/.*\/works/
                ],
                () => {
                    const worksListHandler = new WorksListHandler();
                }
            );

            // Handler for user preferences page
            urlHandler.addHandler(
                [/\/users\/.+\/preferences/],
                () => {
                    const settingsPage = new SettingsPageHandler(this.settings);
                }
            );

            // Execute handler based on the current URL
            const currentPath = window.location.href;
            urlHandler.matchAndHandle(currentPath);
        }

    }


    // Instantiate the FicTracker class
    const ficTracker = new FicTracker();

})();