AMC Rating Helper

Display Rotten Tomatoes and IMDb ratings next to movie titles on amctheatres site

// ==UserScript==
// @name         AMC Rating Helper
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Display Rotten Tomatoes and IMDb ratings next to movie titles on amctheatres site
// @author       wu5bocheng
// @match        *://www.amctheatres.com/movie-theatres/*
// @match        *://www.amctheatres.com/movies/*
// @match        *://www.amctheatres.com/movies
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_openInTab
// @license      MIT
// ==/UserScript==

/*
SETUP INSTRUCTIONS:
1. Get a free OMDb API key from https://www.omdbapi.com/apikey.aspx
2. Replace 'YOUR_OMDB_API_KEY_HERE' with your actual API key
3. Install the script in Tampermonkey/Greasemonkey
4. Visit AMC Theatres website to see ratings displayed next to movie titles

FEATURES:
- Shows IMDb ratings (via OMDb API)
- Shows Rotten Tomatoes ratings (via OMDb API)
- Shows box office information when available
- Hover tooltips with detailed movie information
- Intelligent caching (24-hour cache duration)
- Instant display for previously loaded movies
- Rate limiting to avoid overwhelming APIs
- Loading indicators while fetching data
- Error handling for failed requests
- Clickable ratings that open the respective service
*/
(function () {
    'use strict';

    // Storage keys
    const STORAGE_KEYS = {
        omdbApiKey: 'amc_omdb_api_key'
    };

    // Helpers to get/set API key
    function getStoredApiKey() {
        try { return (GM_getValue(STORAGE_KEYS.omdbApiKey, '') || '').trim(); } catch { return ''; }
    }
    function setStoredApiKey(key) {
        try { GM_setValue(STORAGE_KEYS.omdbApiKey, (key || '').trim()); } catch {}
    }

    // Setup tab rendering (hash or query param triggers it)
    function isSetupMode() {
        try {
            const hash = (location.hash || '').toLowerCase();
            if (hash.includes('#amc-omdb-setup')) return true;
            const qs = new URLSearchParams(location.search);
            return qs.get('amc-omdb-setup') === '1';
        } catch { return false; }
    }

    function renderSetupPage() {
        const existing = document.getElementById('amc-omdb-setup-root');
        if (existing) return;
        const root = document.createElement('div');
        root.id = 'amc-omdb-setup-root';
        root.setAttribute('style', `
            position: fixed; inset: 0; background: #0b1020; color: #fff; z-index: 2147483647;
            display: flex; align-items: center; justify-content: center; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
        `);
        const panel = document.createElement('div');
        panel.setAttribute('style', `
            width: min(560px, 92vw); background: #121a34; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,.5);
            padding: 24px; border: 1px solid rgba(255,255,255,.08);
        `);
        panel.innerHTML = `
            <div style="display:flex; align-items:center; gap:12px; margin-bottom:16px;">
                <div style="width:36px; height:36px; border-radius:8px; background:#2a5298; display:flex; align-items:center; justify-content:center; font-weight:800;">AMC</div>
                <div style="font-size:18px; font-weight:700;">AMC Rating Helper – Setup</div>
            </div>
            <div style="font-size:14px; opacity:.9; margin-bottom:16px;">
                Enter your free OMDb API key to enable IMDb and Rotten Tomatoes ratings.
                Get one at <a href="https://www.omdbapi.com/apikey.aspx" target="_blank" style="color:#4ea1ff; text-decoration:underline;">omdbapi.com</a>.
            </div>
            <label style="display:block; font-size:12px; opacity:.8; margin-bottom:6px;">OMDb API Key</label>
            <input id="amc-omdb-key-input" type="text" placeholder="e.g. abcd1234" style="width:100%; box-sizing:border-box; padding:10px 12px; border-radius:10px; border:1px solid rgba(255,255,255,.14); background:#0e152b; color:#fff; outline:none;" />
            <div style="display:flex; gap:8px; justify-content:flex-end; margin-top:16px;">
                <button id="amc-omdb-cancel" style="padding:8px 12px; border-radius:10px; background:#263154; color:#fff; border:1px solid rgba(255,255,255,.1); cursor:pointer;">Cancel</button>
                <button id="amc-omdb-save" style="padding:8px 12px; border-radius:10px; background:#2a7ade; color:#fff; border:1px solid #2a7ade; cursor:pointer; font-weight:700;">Save & Apply</button>
            </div>
        `;
        root.appendChild(panel);
        document.documentElement.appendChild(root);

        const input = panel.querySelector('#amc-omdb-key-input');
        input.value = getStoredApiKey();
        const close = () => {
            if (root && root.parentNode) root.parentNode.removeChild(root);
            // Clean URL hash if used
            if (location.hash && location.hash.includes('amc-omdb-setup')) {
                try { history.replaceState(null, '', location.pathname + location.search); } catch {}
            }
        };
        panel.querySelector('#amc-omdb-cancel').addEventListener('click', close);
        panel.querySelector('#amc-omdb-save').addEventListener('click', () => {
            const val = (input.value || '').trim();
            if (!val) {
                input.focus();
                input.style.borderColor = '#d9534f';
                return;
            }
            setStoredApiKey(val);
            close();
            // Optionally refresh to apply immediately
            location.reload();
        });
    }

    // Ensure a menu command to open setup
    try {
        GM_registerMenuCommand('AMC Ratings: Set OMDb API Key…', () => {
            // Use current tab: set hash and render
            try { location.hash = '#amc-omdb-setup'; } catch {}
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', renderSetupPage, { once: true });
            } else {
                renderSetupPage();
            }
        });
    } catch {}

    // First-run onboarding: open setup in current tab if key missing
    try {
        const hasKey = !!getStoredApiKey();
        if (!hasKey) {
            try { location.hash = '#amc-omdb-setup'; } catch {}
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', renderSetupPage, { once: true });
            } else {
                renderSetupPage();
            }
        }
    } catch {}

    // If in setup mode, render setup UI
    if (isSetupMode()) {
        // Defer to ensure DOM is ready
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', renderSetupPage);
        } else {
            renderSetupPage();
        }
    }

    // Use stored key for requests
    function getApiKey() {
        const key = getStoredApiKey();
        return key && key.length > 0 ? key : null;
    }

    // Rate limiting to avoid overwhelming APIs
    const requestQueue = [];
    const maxConcurrentRequests = 10; // Increased for faster processing
    let activeRequests = 0;

    // Cache for storing ratings to avoid repeated API calls
    const ratingsCache = new Map();
    const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours in milliseconds

    // Cache management functions
    function getCacheKey(title) {
        return cleanTitle(title).toLowerCase().trim();
    }

    function getCachedRatings(title) {
        const key = getCacheKey(title);
        const cached = ratingsCache.get(key);

        if (cached && (Date.now() - cached.timestamp) < CACHE_DURATION) {
            return cached.data;
        }

        return null;
    }

    function setCachedRatings(title, ratings) {
        const key = getCacheKey(title);
        ratingsCache.set(key, {
            data: ratings,
            timestamp: Date.now()
        });
    }

    function clearExpiredCache() {
        const now = Date.now();
        for (const [key, value] of ratingsCache.entries()) {
            if (now - value.timestamp >= CACHE_DURATION) {
                ratingsCache.delete(key);
            }
        }
    }

    // Rate limiting function
    function rateLimitedRequest(requestFn) {
        return new Promise((resolve, reject) => {
            requestQueue.push({ requestFn, resolve, reject });
            processQueue();
        });
    }

    function processQueue() {
        if (activeRequests >= maxConcurrentRequests || requestQueue.length === 0) {
            return;
        }

        activeRequests++;
        const { requestFn, resolve, reject } = requestQueue.shift();

        requestFn()
            .then(resolve)
            .catch(reject)
            .finally(() => {
                activeRequests--;
                processQueue();
            });
    }

    function cleanTitle(title) {
        let clean = title.trim();
        // General removals
        clean = clean.replace(/\bQ&A.*$/i, "");
        clean = clean.split("/")[0];
        clean = clean.replace(/[-–]\s*studio ghibli fest\s*\d{4}/gi, "");
        clean = clean.replace(/\bstudio ghibli fest\s*\d{4}/gi, "");
        clean = clean.replace(/\bstudio ghibli fest\b/gi, "");
        clean = clean.replace(/[-–]\s*\d+(st|nd|rd|th)?\s+anniversary\b/gi, "");
        clean = clean.replace(/\b\d+(st|nd|rd|th)?\s+anniversary\b/gi, "");
        clean = clean.replace(/\bunrated\b/gi, "");
        clean = clean.replace(/\b4k\b/gi, "");
        clean = clean.replace(/\b3d\b/gi, "");
        clean = clean.replace(/\bfathom\s*\d{4}\b/gi, "");
        clean = clean.replace(/\bfathom\b/gi, "");
        clean = clean.replace(/\bopening night fan event\b/gi, "");
        clean = clean.replace(/\bopening night\b/gi, "");
        clean = clean.replace(/\bfan event\b/gi, "");
        clean = clean.replace(/\bspecial screening\b/gi, "");
        clean = clean.replace(/\bdouble feature\b/gi, "");
        clean = clean.replace(/\([^)]*\)/g, "");
        // Targeted cleanups
        const cutPhrases = [
            " - Opening Weekend Event",
            "Private Theatre",
            "Early Access",
            "Sneak Peak",
            "IMAX",
            "Sensory Friendly Screening"
        ];
        for (let phrase of cutPhrases) {
            const regex = new RegExp(`[:\\-]?\\s*${phrase}.*$`, "i");
            clean = clean.replace(regex, "");
        }
        return clean.trim();
    }

    // Helper function to parse OMDb data consistently
    function parseOMDbData(data) {
        const imdb = data.imdbRating || null;
        const imdbId = data.imdbID || null;
        const rottenTomatoes = data.Ratings?.find(r => r.Source === 'Rotten Tomatoes')?.Value || null;
        const boxOffice = data.BoxOffice || null;

        // Extract additional movie details for tooltip
        const movieDetails = {
            title: data.Title,
            year: data.Year,
            rated: data.Rated,
            released: data.Released,
            runtime: data.Runtime,
            genre: data.Genre,
            director: data.Director,
            writer: data.Writer,
            actors: data.Actors,
            plot: data.Plot,
            language: data.Language,
            country: data.Country,
            awards: data.Awards,
            poster: data.Poster,
            metascore: data.Metascore,
            imdbVotes: data.imdbVotes,
            type: data.Type,
            dvd: data.DVD,
            production: data.Production,
            website: data.Website
        };

        return { imdb, imdbId, rottenTomatoes, boxOffice, movieDetails };
    }

    // Create search-friendly title variants
    function createSearchVariants(title) {
        const cleaned = cleanTitle(title);
        const variants = [cleaned];

        // Remove subtitles
        const noSubtitle = cleaned.split(':')[0].trim();
        if (noSubtitle !== cleaned) variants.push(noSubtitle);

        // Remove "The" from beginning
        if (cleaned.startsWith('The ')) variants.push(cleaned.substring(4));

        // Remove descriptive words
        const simplified = cleaned.replace(/\b(movie|film|the movie|infinity castle)\b/gi, '').trim();
        if (simplified !== cleaned && simplified.length > 3) variants.push(simplified);

        return [...new Set(variants)];
    }

    // Search for movie using OMDb search API
    function searchOMDbMovie(title) {
        const variants = createSearchVariants(title);

        async function tryVariant(index) {
            if (index >= variants.length) return null;

            const API_KEY = getApiKey();
            if (!API_KEY) return null;

            const searchTitle = variants[index];
            return new Promise((resolve) => {
                const searchUrl = `https://www.omdbapi.com/?s=${encodeURIComponent(searchTitle)}&apikey=${API_KEY}&type=movie`;

                GM_xmlhttpRequest({
                    method: 'GET',
                    url: searchUrl,
                    onload: function(response) {
                        try {
                            const data = JSON.parse(response.responseText);
                            if (data.Response === 'True' && data.Search && data.Search.length > 0) {
                                resolve(data.Search[0].imdbID);
                            } else {
                                tryVariant(index + 1).then(resolve);
                            }
                        } catch (error) {
                            tryVariant(index + 1).then(resolve);
                        }
                    },
                    onerror: function() {
                        tryVariant(index + 1).then(resolve);
                    }
                });
            });
        }

        return rateLimitedRequest(() => tryVariant(0));
    }

    // Fetch detailed movie info by IMDb ID
    function fetchOMDbByID(imdbId) {
        return rateLimitedRequest(() => {
            return new Promise((resolve) => {
                const API_KEY = getApiKey();
                if (!API_KEY) {
                    resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null });
                    return;
                }
                const url = `https://www.omdbapi.com/?i=${imdbId}&apikey=${API_KEY}`;

                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    onload: function(response) {
                        try {
                            const data = JSON.parse(response.responseText);
                            if (data.Response === 'True') {
                                resolve(parseOMDbData(data));
                            } else {
                                resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null });
                            }
                        } catch (error) {
                            resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null });
                        }
                    },
                    onerror: function() {
                        resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null });
                    }
                });
            });
        });
    }

    // Fetch ratings from OMDb API with search fallback
    function fetchOMDbRatings(title) {
        const cached = getCachedRatings(title);
        if (cached && cached.omdb) {
            return Promise.resolve(cached.omdb);
        }

        return rateLimitedRequest(() => {
            return new Promise((resolve) => {
                const API_KEY = getApiKey();
                if (!API_KEY) {
                    console.warn('OMDb API key not set. Open the menu "AMC Ratings: Set OMDb API Key…" to configure.');
                    resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null });
                    return;
                }

                const directUrl = `https://www.omdbapi.com/?t=${encodeURIComponent(title)}&apikey=${API_KEY}`;

                GM_xmlhttpRequest({
                    method: 'GET',
                    url: directUrl,
                    onload: function(response) {
                        try {
                            const data = JSON.parse(response.responseText);
                            if (data.Response === 'True') {
                                const ratings = parseOMDbData(data);
                                const existingCache = getCachedRatings(title) || {};
                                setCachedRatings(title, { ...existingCache, omdb: ratings });
                                resolve(ratings);
                            } else {
                                searchOMDbMovie(title).then(imdbId => {
                                    if (imdbId) {
                                        fetchOMDbByID(imdbId).then(ratings => {
                                            const existingCache = getCachedRatings(title) || {};
                                            setCachedRatings(title, { ...existingCache, omdb: ratings });
                                            resolve(ratings);
                                        }).catch(() => {
                                            resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null });
                                        });
                                    } else {
                                        resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null });
                                    }
                                }).catch(() => {
                                    resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null });
                                });
                            }
                        } catch (error) {
                            resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null });
                        }
                    },
                    onerror: function() {
                        resolve({ imdb: null, imdbId: null, rottenTomatoes: null, boxOffice: null, movieDetails: null });
                    }
                });
            });
        });
    }


    // Rotten Tomatoes slug
    function makeRtSlug(title) {
        let clean = cleanTitle(title).toLowerCase();
        clean = clean.replace(/&/g, " and ");
        clean = clean.replace(/[':;!?,.\-–]/g, "");
        clean = clean.replace(/\s+/g, "_");
        clean = clean.replace(/^_+|_+$/g, "");
        return clean.trim();
    }


    // Create rating display element
    function createRatingElement(service, rating, url, movieDetails = null) {
        const container = document.createElement("div");
        container.className = `${service}-rating`;
        container.style.cssText = `
            display: inline-flex;
            align-items: center;
            margin-left: 8px;
            padding: 4px 8px;
            border-radius: 12px;
            font-size: 12px;
            font-weight: bold;
            text-decoration: none;
            cursor: pointer;
            transition: all 0.2s ease;
            box-shadow: 0 1px 3px rgba(0,0,0,0.2);
            position: relative;
            user-select: none;
            -webkit-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            pointer-events: auto;
        `;

        if (url) {
            container.onclick = (e) => {
                e.stopPropagation();
                e.preventDefault();
                window.open(url, '_blank');
            };
        }

        // Prevent event bubbling to parent elements
        container.onmousedown = (e) => e.stopPropagation();
        container.onmouseup = (e) => e.stopPropagation();

        // Simplified hover handling - tooltip won't interfere due to pointer-events: none
        let showTimeout = null;
        let hideTimeout = null;
        let isShowing = false;

        container.addEventListener('mouseenter', (e) => {
            e.stopPropagation();

            if (isShowing) return;


            // Clear any pending hide timeout
            if (hideTimeout) {
                clearTimeout(hideTimeout);
                hideTimeout = null;
            }

            if (movieDetails) {
                showTimeout = setTimeout(() => {
                    if (!isShowing) {
                        showTooltip(container, movieDetails);
                        isShowing = true;
                    }
                }, 150); // Reduced delay for faster response
            }
        });

        container.addEventListener('mouseleave', (e) => {
            e.stopPropagation();


            // Clear any pending show timeout
            if (showTimeout) {
                clearTimeout(showTimeout);
                showTimeout = null;
            }

            if (movieDetails && isShowing) {
                hideTimeout = setTimeout(() => {
                    hideTooltip(container);
                    isShowing = false;
                }, 200);
            }
        });

        // Set colors and content based on service
        switch (service) {
            case 'imdb':
                container.style.backgroundColor = '#f5c518';
                container.style.color = '#000';
                container.innerHTML = `IMDb: ${rating || 'N/A'}`;
                break;
            case 'rotten':
                container.style.backgroundColor = '#d92323';
                container.style.color = '#fff';
                container.innerHTML = `🍅 ${rating || 'N/A'}`;
                break;
        }

        return container;
    }

    // Create loading indicator
    function createLoadingElement() {
        const loading = document.createElement("div");
        loading.className = "ratings-loading";
        loading.style.cssText = `
            display: inline-flex;
            align-items: center;
            margin-left: 8px;
            padding: 4px 8px;
            border-radius: 12px;
            font-size: 12px;
            color: #666;
            background-color: #f0f0f0;
        `;
        loading.textContent = "Loading ratings...";
        return loading;
    }

    // Helper function to validate box office data
    function isValidBoxOffice(boxOffice) {
        if (!boxOffice) {
            return false;
        }
        if (typeof boxOffice !== 'string') {
            return false;
        }
        const trimmed = boxOffice.trim();
        if (trimmed === '' || trimmed === 'N/A' || trimmed === 'null' || trimmed === 'undefined') {
            return false;
        }
        // Check if it contains any currency symbols or numbers
        const isValid = /[\$£€¥₹]|\d/.test(trimmed);
        return isValid;
    }

    // Create box office element
    function createBoxOfficeElement(boxOffice) {
        // Clean up box office value
        let cleanBoxOffice = boxOffice;
        if (typeof cleanBoxOffice === 'string') {
            cleanBoxOffice = cleanBoxOffice.trim();
        }

        const container = document.createElement("div");
        container.className = "box-office";
        container.style.cssText = `
            display: inline-flex;
            align-items: center;
            margin-left: 8px;
            padding: 4px 8px;
            border-radius: 12px;
            font-size: 12px;
            font-weight: bold;
            text-decoration: none;
            cursor: default;
            transition: all 0.2s ease;
            box-shadow: 0 1px 3px rgba(0,0,0,0.2);
            background-color: #4caf50;
            color: white;
        `;
        container.innerHTML = `💰 ${cleanBoxOffice}`;

        // Prevent event bubbling
        container.onmousedown = (e) => e.stopPropagation();
        container.onmouseup = (e) => e.stopPropagation();
        container.onmouseover = (e) => e.stopPropagation();
        container.onmouseout = (e) => e.stopPropagation();

        return container;
    }

    // Create tooltip element
    function createTooltip(movieDetails) {
        if (!movieDetails) return null;

        const tooltip = document.createElement("div");
        tooltip.className = "movie-tooltip";
        tooltip.style.cssText = `
            position: absolute;
            background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
            color: white;
            padding: 16px;
            border-radius: 12px;
            box-shadow: 0 8px 32px rgba(0,0,0,0.3);
            z-index: 9999;
            max-width: 400px;
            font-size: 13px;
            line-height: 1.4;
            opacity: 1;
            transform: translateY(0);
            pointer-events: auto;
            border: 1px solid rgba(255,255,255,0.1);
            visibility: visible;
            display: block;
        `;

        // Create tooltip content
        let content = `<div style="display: flex; gap: 12px; margin-bottom: 12px;">`;

        // Poster
        if (movieDetails.poster && movieDetails.poster !== 'N/A') {
            content += `
                <img src="${movieDetails.poster}"
                     style="width: 80px; height: 120px; object-fit: cover; border-radius: 6px; flex-shrink: 0;"
                     onerror="this.style.display='none'">
            `;
        }

        // Basic info
        content += `
            <div style="flex: 1;">
                <div style="font-size: 16px; font-weight: bold; margin-bottom: 8px; color: #ffd700;">
                    ${movieDetails.title} (${movieDetails.year})
                </div>
                <div style="margin-bottom: 4px;"><strong>Rated:</strong> ${movieDetails.rated || 'N/A'}</div>
                <div style="margin-bottom: 4px;"><strong>Runtime:</strong> ${movieDetails.runtime || 'N/A'}</div>
                <div style="margin-bottom: 4px;"><strong>Genre:</strong> ${movieDetails.genre || 'N/A'}</div>
                <div style="margin-bottom: 4px;"><strong>Released:</strong> ${movieDetails.released || 'N/A'}</div>
            </div>
        </div>`;

        // Plot
        if (movieDetails.plot && movieDetails.plot !== 'N/A') {
            content += `<div style="margin-bottom: 12px;"><strong>Plot:</strong><br>${movieDetails.plot}</div>`;
        }

        // Crew and cast
        content += `<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px;">`;

        if (movieDetails.director && movieDetails.director !== 'N/A') {
            content += `<div><strong>Director:</strong><br>${movieDetails.director}</div>`;
        }

        if (movieDetails.actors && movieDetails.actors !== 'N/A') {
            const actors = movieDetails.actors.split(',').slice(0, 3).join(', ');
            content += `<div><strong>Cast:</strong><br>${actors}${movieDetails.actors.split(',').length > 3 ? '...' : ''}</div>`;
        }

        content += `</div>`;

        // Additional info
        if (movieDetails.awards && movieDetails.awards !== 'N/A') {
            content += `<div style="margin-bottom: 8px;"><strong>Awards:</strong> ${movieDetails.awards}</div>`;
        }

        if (movieDetails.country && movieDetails.country !== 'N/A') {
            content += `<div style="margin-bottom: 8px;"><strong>Country:</strong> ${movieDetails.country}</div>`;
        }

        if (movieDetails.language && movieDetails.language !== 'N/A') {
            content += `<div style="margin-bottom: 8px;"><strong>Language:</strong> ${movieDetails.language}</div>`;
        }

        tooltip.innerHTML = content;

        // Tooltip has pointer-events: none, so no mouse handlers needed

        return tooltip;
    }

    // Show tooltip
    function showTooltip(element, movieDetails) {
        if (!movieDetails) {
            return;
        }

        // Check if tooltip already exists
        if (element._tooltip) {
            return;
        }

        // Hide any existing tooltip first
        hideTooltip(element);

        const tooltip = createTooltip(movieDetails);
        if (!tooltip) return;

        // Force a reflow to get accurate dimensions
        tooltip.offsetHeight;

        const rect = element.getBoundingClientRect();
        const tooltipRect = tooltip.getBoundingClientRect();


        // Position tooltip
        let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
        let top = rect.top - tooltipRect.height - 10;

        // Adjust if tooltip goes off screen
        if (left < 10) left = 10;
        if (left + tooltipRect.width > window.innerWidth - 10) {
            left = window.innerWidth - tooltipRect.width - 10;
        }
        if (top < 10) {
            top = rect.bottom + 10;
        }

        tooltip.setAttribute('style', `
            position: fixed !important;
            left: ${left}px !important;
            top: ${top}px !important;
            background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%) !important;
            color: white !important;
            padding: 16px !important;
            border-radius: 12px !important;
            box-shadow: 0 8px 32px rgba(0,0,0,0.3) !important;
            z-index: 999999 !important;
            max-width: 400px !important;
            font-size: 13px !important;
            line-height: 1.4 !important;
            opacity: 1 !important;
            visibility: visible !important;
            display: block !important;
            border: 1px solid rgba(255,255,255,0.1) !important;
            pointer-events: none !important;
        `);

        // Append to documentElement for maximum compatibility
        document.documentElement.appendChild(tooltip);



        // Store reference for cleanup
        element._tooltip = tooltip;
    }

    // Hide tooltip
    function hideTooltip(element) {
        if (element._tooltip) {
            element._tooltip.style.opacity = '0';
            element._tooltip.style.transform = 'translateY(10px)';
            setTimeout(() => {
                if (element._tooltip && element._tooltip.parentNode) {
                    element._tooltip.parentNode.removeChild(element._tooltip);
                }
                element._tooltip = null;
                // Reset hovering state
                element.isHovering = false;
            }, 300);
        }
    }

    async function addRatings() {
        const movieTitleEls = document.querySelectorAll(".md\\:text-2xl.font-bold, h3 > a.headline, h1.headline");

        movieTitleEls.forEach(async (movieTitleEl) => {
            const rawTitle = movieTitleEl.textContent.trim();
            if (!rawTitle || /amc/i.test(rawTitle)) return;
            if (movieTitleEl.querySelector(".ratings-container")) return;

            const cleanTitleText = cleanTitle(rawTitle);

            // Check if we have all ratings cached
            const cached = getCachedRatings(cleanTitleText);
            const hasAllCached = cached &&
                cached.omdb &&
                (cached.omdb.imdb !== null || cached.omdb.rottenTomatoes !== null);

            // Create container for all ratings
            const ratingsContainer = document.createElement("div");
            ratingsContainer.className = "ratings-container";
            ratingsContainer.style.cssText = `
                display: inline-flex;
                align-items: center;
                margin-left: 8px;
                gap: 4px;
                position: relative;
                z-index: 10;
            `;

            // Prevent event bubbling to parent elements (AMC navigation)
            ratingsContainer.onmousedown = (e) => e.stopPropagation();
            ratingsContainer.onmouseup = (e) => e.stopPropagation();
            ratingsContainer.onclick = (e) => e.stopPropagation();

            // If we have cached data, show it immediately
            if (hasAllCached) {
                // Use IMDb ID if available, otherwise fall back to search
                const imdbUrl = cached.omdb.imdbId
                    ? `https://www.imdb.com/title/${cached.omdb.imdbId}/`
                    : `https://www.imdb.com/find/?q=${encodeURIComponent(cleanTitleText)}`;
                const rtSlug = makeRtSlug(rawTitle);
                const rtUrl = `https://www.rottentomatoes.com/m/${rtSlug}`;

                // Add IMDb rating
                const imdbEl = createRatingElement('imdb', cached.omdb.imdb, imdbUrl, cached.omdb.movieDetails);
                ratingsContainer.appendChild(imdbEl);

                // Add Rotten Tomatoes rating
                const rtEl = createRatingElement('rotten', cached.omdb.rottenTomatoes, rtUrl, cached.omdb.movieDetails);
                ratingsContainer.appendChild(rtEl);

                // Add box office if available
                if (isValidBoxOffice(cached.omdb.boxOffice)) {
                    const boxOfficeEl = createBoxOfficeElement(cached.omdb.boxOffice);
                    ratingsContainer.appendChild(boxOfficeEl);
                }

                movieTitleEl.appendChild(ratingsContainer);
                return;
            }

            // Add loading indicator for non-cached data
            const loadingEl = createLoadingElement();
            ratingsContainer.appendChild(loadingEl);
            movieTitleEl.appendChild(ratingsContainer);

            try {
                // Fetch ratings
                const omdbRatings = await fetchOMDbRatings(cleanTitleText);

                // Remove loading indicator
                ratingsContainer.removeChild(loadingEl);

                // Create rating elements
                // Use IMDb ID if available, otherwise fall back to search
                const imdbUrl = omdbRatings.imdbId
                    ? `https://www.imdb.com/title/${omdbRatings.imdbId}/`
                    : `https://www.imdb.com/find/?q=${encodeURIComponent(cleanTitleText)}`;
                const rtSlug = makeRtSlug(rawTitle);
                const rtUrl = `https://www.rottentomatoes.com/m/${rtSlug}`;

                // Add IMDb rating
                const imdbEl = createRatingElement('imdb', omdbRatings.imdb, imdbUrl, omdbRatings.movieDetails);
                ratingsContainer.appendChild(imdbEl);

                // Add Rotten Tomatoes rating
                const rtEl = createRatingElement('rotten', omdbRatings.rottenTomatoes, rtUrl, omdbRatings.movieDetails);
                ratingsContainer.appendChild(rtEl);

                // Add box office if available
                if (isValidBoxOffice(omdbRatings.boxOffice)) {
                    const boxOfficeEl = createBoxOfficeElement(omdbRatings.boxOffice);
                    ratingsContainer.appendChild(boxOfficeEl);
                }

            } catch (error) {
                // Remove loading and show error
                ratingsContainer.removeChild(loadingEl);
                const errorEl = document.createElement("div");
                errorEl.style.cssText = `
                    display: inline-flex;
                    align-items: center;
                    margin-left: 8px;
                    padding: 4px 8px;
                    border-radius: 12px;
                    font-size: 12px;
                    color: #d32f2f;
                    background-color: #ffebee;
                `;
                errorEl.textContent = "Error loading ratings";
                ratingsContainer.appendChild(errorEl);
            }
        });
    }

    // Clean up tooltips
    function cleanupTooltips() {
        const tooltips = document.querySelectorAll('.movie-tooltip');
        tooltips.forEach(tooltip => {
            if (tooltip.parentNode) {
                tooltip.parentNode.removeChild(tooltip);
            }
        });
    }

    // Initialize script
    clearExpiredCache(); // Clean up expired cache entries on startup
    addRatings();
    const observer = new MutationObserver(() => {
        cleanupTooltips(); // Clean up tooltips when DOM changes
        addRatings();
    });
    observer.observe(document.body, { childList: true, subtree: true });
})();