// ==UserScript==
// @name         StoryGraph Personalization Enhancer
// @namespace    http://tampermonkey.net/
// @version      1.8
// @description  Enhance StoryGraph's personalized recommendations
// @author       You
// @match        https://app.thestorygraph.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      api.openai.com
// @connect      blue.thestorygraph.com
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const DEBUG = false;
    let isProcessing = false;

    function log(...args) {
        if (DEBUG) {
            console.log('%c[StoryGraph Enhancer]', 'color: #1FB784;', ...args);
        }
    }

    function isVisible(element) {
        return element &&
               element.offsetParent !== null &&
               window.getComputedStyle(element).display !== 'none';
    }

    function findTurboFrames() {
        const desktopFrame = document.querySelector('turbo-frame[id^="personalized-preview-desktop-"]');
        const mobileFrame = document.querySelector('turbo-frame[id^="personalized-preview-mobile-"]');

        if (desktopFrame || mobileFrame) {
            log('Found frames:', {
                desktop: desktopFrame?.id || 'none',
                mobile: mobileFrame?.id || 'none'
            });
        }

        return { desktopFrame, mobileFrame };
    }

    function findPersonalizedTab() {
        const tabs = document.querySelectorAll('a[data-personalized="true"]');
        return Array.from(tabs).find(tab => isVisible(tab));
    }

    async function waitForContent(frame, maxWaitTime = 60000) {
        if (!frame) return null;

        const startTime = Date.now();
        let attempt = 0;

        log('Waiting for content in frame:', frame.id);

        while (Date.now() - startTime < maxWaitTime) {
            // First check if frame itself has content
            if (frame.textContent?.trim()) {
                log('Frame has direct content');
                return frame;
            }

            // Then check all direct children
            const children = frame.children;
            for (const child of children) {
                if (child.textContent?.trim()) {
                    log('Found content in child:', child);
                    return child;
                }
            }

            log(`Attempt ${++attempt}: No content yet...`);
            await new Promise(resolve => setTimeout(resolve, 200));
        }

        log('Timeout reached in waitForContent for frame:', frame.id);
        return null;
    }

    async function clickPersonalizedTab() {
        const tab = findPersonalizedTab();
        if (!tab) throw new Error('Personalized tab not found');

        const isActive = tab.classList.contains('border-darkerGrey') ||
                         tab.classList.contains('border-lightGrey');

        if (!isActive) {
            log('Clicking personalized tab...');
            tab.click();

            // Wait a moment for frames to update
            await new Promise(resolve => setTimeout(resolve, 500));
        } else {
            log('Personalized tab already active');
        }

        // Find frames after click/check
        const { desktopFrame, mobileFrame } = findTurboFrames();
        const targetFrame = desktopFrame || mobileFrame;

        if (!targetFrame) {
            throw new Error('No turbo frame found');
        }

        log('Waiting for content in frame:', targetFrame.id);
        const content = await waitForContent(targetFrame, 5000);

        if (!content) {
            throw new Error('No content found in frame');
        }

        return content;
    }

    async function enhanceWithGPT(originalText) {
        log('Calling GPT API...');
        try {
            // First check if we have an API key
            const apiKey = GM_getValue('openai_api_key');
            if (!apiKey) {
                const key = prompt('Please enter your OpenAI API key:');
                if (!key) throw new Error('No API key provided');
                GM_setValue('openai_api_key', key);
            }

            const response = await fetch('https://api.openai.com/v1/chat/completions', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${GM_getValue('openai_api_key')}`
                },
                body: JSON.stringify({
                    model: 'chatgpt-4o-latest',
                    messages: [{
                        role: 'system',
                        content: `You are an enthusiastic, opinionated book recommender who transforms formal book recommendations into engaging, conversational recommendations. When rewriting recommendations:

1. Use a confident, direct tone as if you're a knowledgeable friend giving advice. Make strong statements about whether the reader will likely enjoy or dislike the book based on their reading history.

2. Structure your response in this order:
- Start with your strongest opinion about fit/non-fit, directly referencing specific books and ratings from their history
- Follow with a clear "BUT" or "catch" that acknowledges potential mismatches
- End with a decisive "Bottom line" recommendation

3. Style guidelines:
- Use conversational language ("Look," "Here's the thing," "Quick reality check")
- Make bold predictions based on rating patterns (e.g., "if you loved X (4.75!), you'll definitely...")
- Acknowledge both positive and negative preference patterns
- Reference specific ratings to build credibility
- Use em-dashes, parentheticals, and exclamation points for emphasis
- Keep paragraphs short and punchy
- Do not use any markdown formatting - the text will be displayed as plain text with line breaks

4. Transform formal aspects:
- Change "Aspects you might like" into direct predictions
- Replace neutral phrases ("suggests," "indicates") with stronger ones ("proves," "tells me everything")
- Convert dry analysis into emotional reactions
- Use the reader's rating history as evidence for your predictions

5. Tone:
- Be enthusiastic but honest about potential issues
- Show that you've really analyzed their reading patterns
- Make clear calls about whether they should read it now, save it for later, or approach with caution
- Don't hedge unless there's genuine uncertainty based on mixed ratings

6. Length:
- Aim for 3-4 short paragraphs maximum
- Always end with a "Bottom line" summary of 1-2 sentences
- Keep total length under 200 words`
                    }, {
                        role: 'user',
                        content: `Transform this clinical, bullet-pointed recommendation into a conversational, opinionated recommendation that feels like it's coming from a friend who knows your reading tastes intimately and isn't afraid to make bold predictions based on your reading history:\n\n${originalText}`
                    }]
                })
            });

            const data = await response.json();
            if (!response.ok) {
                throw new Error(`API error: ${data.error?.message || 'Unknown error'}`);
            }

            log('GPT API response received');
            return data.choices[0].message.content.trim();
        } catch (error) {
            log('GPT API error:', error);
            throw error;
        }
    }

    async function handleBookPage() {
        if (isProcessing) {
            log('Already processing, skipping...');
            return;
        }

        isProcessing = true;
        log('Starting book page processing...');

        try {
            await clickPersonalizedTab();

            const turboFrameObserver = new MutationObserver((mutations) => {
                mutations.forEach((mutation) => {
                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        const content = mutation.target.querySelector('.trix-content, .preview-content');
                        if (content && content.textContent.trim()) {
                            log('Content updated via Turbo frame');
                            enhanceContent(content);
                        }
                    }
                });
            });

            const { desktopFrame, mobileFrame } = findTurboFrames();

            [desktopFrame, mobileFrame].forEach(frame => {
                if (frame) {
                    turboFrameObserver.observe(frame, {
                        childList: true,
                        subtree: true
                    });
                }
            });

            const desktopContent = await waitForContent(desktopFrame);
            const mobileContent = await waitForContent(mobileFrame);

            const content = desktopContent || mobileContent;

            if (!content) {
                throw new Error('Content not found after waiting');
            }

            await enhanceContent(content);

        } catch (error) {
            log('Error during processing:', error.message);
        } finally {
            isProcessing = false;
        }
    }

    async function enhanceContent(content) {
        if (!content) return;

        const originalText = content.textContent.trim();
        log('Found content:', originalText.substring(0, 100) + '...');

        try {
            const enhancedText = await enhanceWithGPT(originalText);

            // Create enhanced div while preserving original styles
            const enhancedDiv = document.createElement('div');
            enhancedDiv.className = 'enhanced-content';
            enhancedDiv.innerHTML = enhancedText.replace(/\n/g, '<br>');

            // Copy the original element's classes to preserve styling
            const originalClasses = content.className;
            content.innerHTML = ''; // Clear existing content

            // Preserve the original element's classes
            content.className = originalClasses;

            // Add subtle styling that doesn't override the theme
            enhancedDiv.style.cssText = `
                padding: 15px;
                border-radius: 8px;
                line-height: 1.6;
                background: inherit;
                color: inherit;
                font-size: 16px;
            `;

            content.appendChild(enhancedDiv);
            log('Enhancement complete!');

        } catch (error) {
            log('Enhancement failed:', error);
        }
    }

    function setupPageObserver() {
        document.addEventListener('turbo:load', () => {
            if (window.location.pathname.startsWith('/books/')) {
                setTimeout(() => handleBookPage(), 1000);
            }
        });

        const observer = new MutationObserver((mutations) => {
            if (!window.location.pathname.startsWith('/books/') || isProcessing) return;

            const hasNewFrame = mutations.some(mutation =>
                Array.from(mutation.addedNodes).some(node =>
                    node.nodeName === 'TURBO-FRAME' ||
                    (node.nodeType === 1 && node.querySelector?.('turbo-frame'))
                )
            );

            if (hasNewFrame) {
                setTimeout(() => handleBookPage(), 1000);
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['id', 'class'],
        });

        if (window.location.pathname.startsWith('/books/')) {
            setTimeout(() => handleBookPage(), 1000);
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', setupPageObserver);
    } else {
        setupPageObserver();
    }
})();