import { Controller } from '@hotwired/stimulus';

/**
 * SEO Analyzer Stimulus Controller
 *
 * Real-time SEO analysis matching the server-side SeoScorer service.
 * Monitors title, description (spot), content, focus keyword, and slug fields
 * and provides a visual score bar, individual check results, and a SERP preview.
 *
 * Targets:
 *   title       - The news title input
 *   description - The meta description / spot textarea
 *   content     - The main content (textarea or CKEditor hidden input)
 *   keyword     - The focus keyword input
 *   slug        - The URL slug input
 *   scoreBar    - The progress bar element for visual score
 *   scoreValue  - The element showing numeric score
 *   gradeValue  - The element showing letter grade
 *   checksList  - The container for individual check results
 *   serpPreview - The container for the Google SERP preview
 */
export default class extends Controller {
    static targets = [
        'title',
        'description',
        'content',
        'keyword',
        'slug',
        'scoreBar',
        'scoreValue',
        'gradeValue',
        'checksList',
        'serpPreview',
    ];

    static values = {
        siteUrl: { type: String, default: 'https://example.com' },
    };

    connect() {
        this._debounceTimer = null;
        this._boundAnalyze = this._debouncedAnalyze.bind(this);

        // Attach input listeners to all field targets
        this._attachListeners();

        // Run initial analysis
        this._runAnalysis();
    }

    disconnect() {
        this._detachListeners();
        if (this._debounceTimer) {
            clearTimeout(this._debounceTimer);
        }
    }

    // ---------------------------------------------------------------
    // Listener management
    // ---------------------------------------------------------------

    _attachListeners() {
        const fields = ['title', 'description', 'content', 'keyword', 'slug'];
        fields.forEach((name) => {
            const target = this._getTarget(name);
            if (target) {
                target.addEventListener('input', this._boundAnalyze);
                target.addEventListener('change', this._boundAnalyze);
            }
        });

        // CKEditor support: observe mutations on the content target so we can
        // pick up changes dispatched programmatically.
        if (this.hasContentTarget) {
            this._observer = new MutationObserver(this._boundAnalyze);
            this._observer.observe(this.contentTarget, {
                attributes: true,
                childList: false,
                characterData: false,
            });
        }
    }

    _detachListeners() {
        const fields = ['title', 'description', 'content', 'keyword', 'slug'];
        fields.forEach((name) => {
            const target = this._getTarget(name);
            if (target) {
                target.removeEventListener('input', this._boundAnalyze);
                target.removeEventListener('change', this._boundAnalyze);
            }
        });

        if (this._observer) {
            this._observer.disconnect();
        }
    }

    _getTarget(name) {
        const has = `has${name.charAt(0).toUpperCase()}${name.slice(1)}Target`;
        return this[has] ? this[`${name}Target`] : null;
    }

    // ---------------------------------------------------------------
    // Debounce
    // ---------------------------------------------------------------

    _debouncedAnalyze() {
        if (this._debounceTimer) {
            clearTimeout(this._debounceTimer);
        }
        this._debounceTimer = setTimeout(() => this._runAnalysis(), 300);
    }

    // ---------------------------------------------------------------
    // Field value helpers
    // ---------------------------------------------------------------

    _getFieldValue(name) {
        const target = this._getTarget(name);
        if (!target) return '';
        return (target.value || '').trim();
    }

    get _title() {
        return this._getFieldValue('title');
    }

    get _description() {
        return this._getFieldValue('description');
    }

    get _content() {
        return this._getFieldValue('content');
    }

    get _keyword() {
        return this._getFieldValue('keyword');
    }

    get _slug() {
        return this._getFieldValue('slug');
    }

    // ---------------------------------------------------------------
    // Main analysis orchestrator
    // ---------------------------------------------------------------

    _runAnalysis() {
        const checks = [];

        this._checkTitleLength(checks);
        this._checkMetaDescriptionLength(checks);
        this._checkFocusKeywordInTitle(checks);
        this._checkFocusKeywordInDescription(checks);
        this._checkFocusKeywordInUrl(checks);
        this._checkContentLength(checks);
        this._checkKeywordDensity(checks);
        this._checkInternalLinks(checks);
        this._checkExternalLinks(checks);
        this._checkImageAltText(checks);
        this._checkHeadingHierarchy(checks);
        this._checkKeywordInFirstParagraph(checks);

        const totalScore = checks.reduce((sum, c) => sum + c.points, 0);
        const grade = this._calculateGrade(totalScore);

        this._renderScoreBar(totalScore);
        this._renderGrade(grade);
        this._renderChecks(checks);
        this._renderSerpPreview();
    }

    _calculateGrade(score) {
        if (score >= 80) return 'A';
        if (score >= 60) return 'B';
        if (score >= 40) return 'C';
        if (score >= 20) return 'D';
        return 'F';
    }

    // ---------------------------------------------------------------
    // Utility helpers
    // ---------------------------------------------------------------

    _stripTags(html) {
        const tmp = document.createElement('div');
        tmp.innerHTML = html;
        return tmp.textContent || tmp.innerText || '';
    }

    _countWords(text) {
        const cleaned = text.replace(/\s+/g, ' ').trim();
        if (cleaned === '') return 0;
        return cleaned.split(/\s+/).length;
    }

    _countOccurrences(text, keyword) {
        if (!keyword) return 0;
        const lowerText = text.toLowerCase();
        const lowerKw = keyword.toLowerCase();
        let count = 0;
        let pos = 0;
        while ((pos = lowerText.indexOf(lowerKw, pos)) !== -1) {
            count++;
            pos += lowerKw.length;
        }
        return count;
    }

    // ---------------------------------------------------------------
    // 12 SEO checks (mirroring SeoScorer service)
    // ---------------------------------------------------------------

    /** Check 1: title_length (max 10 pts) */
    _checkTitleLength(checks) {
        const title = this._title;
        const length = title.length;

        if (length >= 50 && length <= 60) {
            checks.push({
                key: 'title_length',
                label: 'Baslik Uzunlugu',
                status: 'success',
                message: `Baslik uzunlugu ideal (${length} karakter). 50-60 karakter arasinda.`,
                points: 10,
            });
        } else if (length >= 40 && length <= 70) {
            checks.push({
                key: 'title_length',
                label: 'Baslik Uzunlugu',
                status: 'warning',
                message: `Baslik uzunlugu kabul edilebilir (${length} karakter) ancak 50-60 karakter arasi daha iyi olur.`,
                points: 7,
            });
        } else {
            checks.push({
                key: 'title_length',
                label: 'Baslik Uzunlugu',
                status: 'error',
                message: `Baslik uzunlugu uygun degil (${length} karakter). 50-60 karakter arasi olmalidir.`,
                points: 3,
            });
        }
    }

    /** Check 2: meta_description_length (max 10 pts) */
    _checkMetaDescriptionLength(checks) {
        const desc = this._description;

        if (!desc) {
            checks.push({
                key: 'meta_description_length',
                label: 'Meta Aciklama Uzunlugu',
                status: 'error',
                message: 'Meta aciklama tanimlanmamis. 120-160 karakter arasi bir aciklama ekleyin.',
                points: 3,
            });
            return;
        }

        const length = desc.length;

        if (length >= 120 && length <= 160) {
            checks.push({
                key: 'meta_description_length',
                label: 'Meta Aciklama Uzunlugu',
                status: 'success',
                message: `Meta aciklama uzunlugu ideal (${length} karakter). 120-160 karakter arasinda.`,
                points: 10,
            });
        } else if (length >= 80 && length <= 200) {
            checks.push({
                key: 'meta_description_length',
                label: 'Meta Aciklama Uzunlugu',
                status: 'warning',
                message: `Meta aciklama uzunlugu kabul edilebilir (${length} karakter) ancak 120-160 karakter arasi daha iyi olur.`,
                points: 7,
            });
        } else {
            checks.push({
                key: 'meta_description_length',
                label: 'Meta Aciklama Uzunlugu',
                status: 'error',
                message: `Meta aciklama uzunlugu uygun degil (${length} karakter). 120-160 karakter arasi olmalidir.`,
                points: 3,
            });
        }
    }

    /** Check 3: focus_keyword_in_title (max 10 pts) */
    _checkFocusKeywordInTitle(checks) {
        const keyword = this._keyword;
        const title = this._title;

        if (!keyword) {
            checks.push({
                key: 'focus_keyword_in_title',
                label: 'Odak Anahtar Kelime - Baslik',
                status: 'error',
                message: 'Odak anahtar kelime belirlenmemis. SEO icin bir anahtar kelime belirleyin.',
                points: 0,
            });
            return;
        }

        const found = title.toLowerCase().includes(keyword.toLowerCase());

        checks.push({
            key: 'focus_keyword_in_title',
            label: 'Odak Anahtar Kelime - Baslik',
            status: found ? 'success' : 'error',
            message: found
                ? 'Odak anahtar kelime baslikta bulundu.'
                : 'Odak anahtar kelime baslikta bulunamadi. Basliga anahtar kelimeyi ekleyin.',
            points: found ? 10 : 0,
        });
    }

    /** Check 4: focus_keyword_in_description (max 8 pts) */
    _checkFocusKeywordInDescription(checks) {
        const keyword = this._keyword;
        const desc = this._description;

        if (!keyword) {
            checks.push({
                key: 'focus_keyword_in_description',
                label: 'Odak Anahtar Kelime - Meta Aciklama',
                status: 'error',
                message: 'Odak anahtar kelime belirlenmemis.',
                points: 0,
            });
            return;
        }

        if (!desc) {
            checks.push({
                key: 'focus_keyword_in_description',
                label: 'Odak Anahtar Kelime - Meta Aciklama',
                status: 'error',
                message: 'Meta aciklama tanimlanmamis. Anahtar kelimeyi iceren bir meta aciklama ekleyin.',
                points: 0,
            });
            return;
        }

        const found = desc.toLowerCase().includes(keyword.toLowerCase());

        checks.push({
            key: 'focus_keyword_in_description',
            label: 'Odak Anahtar Kelime - Meta Aciklama',
            status: found ? 'success' : 'error',
            message: found
                ? 'Odak anahtar kelime meta aciklamada bulundu.'
                : 'Odak anahtar kelime meta aciklamada bulunamadi. Meta aciklamaya anahtar kelimeyi ekleyin.',
            points: found ? 8 : 0,
        });
    }

    /** Check 5: focus_keyword_in_url (max 7 pts) */
    _checkFocusKeywordInUrl(checks) {
        const keyword = this._keyword;
        const slug = this._slug;

        if (!keyword) {
            checks.push({
                key: 'focus_keyword_in_url',
                label: 'Odak Anahtar Kelime - URL',
                status: 'error',
                message: 'Odak anahtar kelime belirlenmemis.',
                points: 0,
            });
            return;
        }

        if (!slug) {
            checks.push({
                key: 'focus_keyword_in_url',
                label: 'Odak Anahtar Kelime - URL',
                status: 'error',
                message: "URL (slug) tanimlanmamis. Anahtar kelimeyi iceren bir URL belirleyin.",
                points: 0,
            });
            return;
        }

        const normalizedKeyword = keyword.toLowerCase();
        const normalizedSlug = slug.toLowerCase();
        const slugKeyword = normalizedKeyword.replace(/\s+/g, '-');

        const found =
            normalizedSlug.includes(normalizedKeyword) ||
            normalizedSlug.includes(slugKeyword);

        checks.push({
            key: 'focus_keyword_in_url',
            label: 'Odak Anahtar Kelime - URL',
            status: found ? 'success' : 'error',
            message: found
                ? "Odak anahtar kelime URL'de bulundu."
                : "Odak anahtar kelime URL'de bulunamadi. URL'ye anahtar kelimeyi ekleyin.",
            points: found ? 7 : 0,
        });
    }

    /** Check 6: content_length (max 10 pts) */
    _checkContentLength(checks) {
        const content = this._content;

        if (!content) {
            checks.push({
                key: 'content_length',
                label: 'Icerik Uzunlugu',
                status: 'error',
                message: 'Icerik bulunamadi. SEO icin en az 300 kelimelik icerik olusturun.',
                points: 3,
            });
            return;
        }

        const plainText = this._stripTags(content);
        const wordCount = this._countWords(plainText);

        if (wordCount >= 300) {
            checks.push({
                key: 'content_length',
                label: 'Icerik Uzunlugu',
                status: 'success',
                message: `Icerik uzunlugu ideal (${wordCount} kelime). 300 kelimenin uzerinde.`,
                points: 10,
            });
        } else if (wordCount >= 150) {
            checks.push({
                key: 'content_length',
                label: 'Icerik Uzunlugu',
                status: 'warning',
                message: `Icerik uzunlugu kabul edilebilir (${wordCount} kelime) ancak 300 kelimenin uzerinde olmasi daha iyi olur.`,
                points: 7,
            });
        } else {
            checks.push({
                key: 'content_length',
                label: 'Icerik Uzunlugu',
                status: 'error',
                message: `Icerik cok kisa (${wordCount} kelime). En az 300 kelimelik icerik olusturun.`,
                points: 3,
            });
        }
    }

    /** Check 7: keyword_density (max 8 pts) */
    _checkKeywordDensity(checks) {
        const keyword = this._keyword;
        const content = this._content;

        if (!keyword) {
            checks.push({
                key: 'keyword_density',
                label: 'Anahtar Kelime Yogunlugu',
                status: 'error',
                message: 'Odak anahtar kelime belirlenmemis. Yogunluk hesaplanamadi.',
                points: 2,
            });
            return;
        }

        if (!content) {
            checks.push({
                key: 'keyword_density',
                label: 'Anahtar Kelime Yogunlugu',
                status: 'error',
                message: 'Icerik bulunamadi. Anahtar kelime yogunlugu hesaplanamadi.',
                points: 2,
            });
            return;
        }

        const plainText = this._stripTags(content).toLowerCase();
        const wordCount = this._countWords(plainText);

        if (wordCount === 0) {
            checks.push({
                key: 'keyword_density',
                label: 'Anahtar Kelime Yogunlugu',
                status: 'error',
                message: 'Icerikte kelime bulunamadi.',
                points: 2,
            });
            return;
        }

        const keywordLower = keyword.toLowerCase();
        const keywordOccurrences = this._countOccurrences(plainText, keywordLower);
        const keywordWordCount = this._countWords(keywordLower);
        const density = (keywordOccurrences * keywordWordCount / wordCount) * 100;
        const densityFormatted = density.toFixed(1);

        if (density >= 1.0 && density <= 3.0) {
            checks.push({
                key: 'keyword_density',
                label: 'Anahtar Kelime Yogunlugu',
                status: 'success',
                message: `Anahtar kelime yogunlugu ideal (%${densityFormatted}). %1-3 arasinda.`,
                points: 8,
            });
        } else if (density >= 0.5 && density <= 4.0) {
            checks.push({
                key: 'keyword_density',
                label: 'Anahtar Kelime Yogunlugu',
                status: 'warning',
                message: `Anahtar kelime yogunlugu kabul edilebilir (%${densityFormatted}) ancak %1-3 arasi daha iyi olur.`,
                points: 5,
            });
        } else {
            const status = density < 0.5 ? 'cok dusuk' : 'cok yuksek';
            checks.push({
                key: 'keyword_density',
                label: 'Anahtar Kelime Yogunlugu',
                status: 'error',
                message: `Anahtar kelime yogunlugu ${status} (%${densityFormatted}). %1-3 arasi olmalidir.`,
                points: 2,
            });
        }
    }

    /** Check 8: internal_links (max 7 pts) */
    _checkInternalLinks(checks) {
        const content = this._content;

        if (!content) {
            checks.push({
                key: 'internal_links',
                label: 'Dahili Baglantilar',
                status: 'error',
                message: 'Icerik bulunamadi. Icerige dahili baglantilar ekleyin.',
                points: 0,
            });
            return;
        }

        const hasLinks = content.toLowerCase().includes('<a');

        checks.push({
            key: 'internal_links',
            label: 'Dahili Baglantilar',
            status: hasLinks ? 'success' : 'error',
            message: hasLinks
                ? 'Icerikte baglantilar bulundu.'
                : 'Icerikte hic baglanti bulunamadi. SEO icin dahili baglantilar ekleyin.',
            points: hasLinks ? 7 : 0,
        });
    }

    /** Check 9: external_links (max 5 pts) */
    _checkExternalLinks(checks) {
        const content = this._content;

        if (!content) {
            checks.push({
                key: 'external_links',
                label: 'Harici Baglantilar',
                status: 'error',
                message: 'Icerik bulunamadi. Icerige harici baglantilar ekleyin.',
                points: 0,
            });
            return;
        }

        const hasExternalLinks = /<a\s[^>]*href\s*=\s*["']https?:\/\//i.test(content);

        checks.push({
            key: 'external_links',
            label: 'Harici Baglantilar',
            status: hasExternalLinks ? 'success' : 'warning',
            message: hasExternalLinks
                ? 'Icerikte harici baglantilar bulundu.'
                : 'Icerikte harici baglanti bulunamadi. Guvenilir kaynaklara harici baglantilar ekleyin.',
            points: hasExternalLinks ? 5 : 0,
        });
    }

    /** Check 10: image_alt_text (max 8 pts) */
    _checkImageAltText(checks) {
        const content = this._content;

        if (!content) {
            checks.push({
                key: 'image_alt_text',
                label: 'Gorsel Alt Metni',
                status: 'warning',
                message: 'Icerik bulunamadi. Gorseller ekleyip alt metni tanimlayin.',
                points: 0,
            });
            return;
        }

        const imgRegex = /<img\s[^>]*>/gi;
        const imgMatches = content.match(imgRegex);

        if (!imgMatches || imgMatches.length === 0) {
            checks.push({
                key: 'image_alt_text',
                label: 'Gorsel Alt Metni',
                status: 'warning',
                message: 'Icerikte gorsel bulunamadi. SEO icin gorseller ekleyin ve alt metin tanimlayin.',
                points: 0,
            });
            return;
        }

        const totalImages = imgMatches.length;
        let imagesWithAlt = 0;

        imgMatches.forEach((imgTag) => {
            if (/alt\s*=\s*["'][^"']+["']/i.test(imgTag)) {
                imagesWithAlt++;
            }
        });

        if (imagesWithAlt === totalImages) {
            checks.push({
                key: 'image_alt_text',
                label: 'Gorsel Alt Metni',
                status: 'success',
                message: `Tum gorsellerde alt metin tanimli (${totalImages}/${totalImages}).`,
                points: 8,
            });
        } else if (imagesWithAlt > 0) {
            checks.push({
                key: 'image_alt_text',
                label: 'Gorsel Alt Metni',
                status: 'warning',
                message: `Bazi gorsellerde alt metin eksik (${imagesWithAlt}/${totalImages}). Tum gorsellere alt metin ekleyin.`,
                points: 4,
            });
        } else {
            checks.push({
                key: 'image_alt_text',
                label: 'Gorsel Alt Metni',
                status: 'error',
                message: `Hicbir gorselde alt metin tanimli degil (0/${totalImages}). Tum gorsellere alt metin ekleyin.`,
                points: 0,
            });
        }
    }

    /** Check 11: heading_hierarchy (max 8 pts) */
    _checkHeadingHierarchy(checks) {
        const content = this._content;

        if (!content) {
            checks.push({
                key: 'heading_hierarchy',
                label: 'Baslik Hiyerarsisi',
                status: 'error',
                message: 'Icerik bulunamadi. Icerige H2 ve H3 basliklari ekleyin.',
                points: 0,
            });
            return;
        }

        const hasH2 = /<h2[\s>]/i.test(content);
        const hasH3 = /<h3[\s>]/i.test(content);

        if (hasH2 && hasH3) {
            checks.push({
                key: 'heading_hierarchy',
                label: 'Baslik Hiyerarsisi',
                status: 'success',
                message: 'Icerikte H2 ve H3 basliklari kullanilmis. Baslik hiyerarsisi dogru.',
                points: 8,
            });
        } else if (hasH2 || hasH3) {
            const found = hasH2 ? 'H2' : 'H3';
            const missing = hasH2 ? 'H3' : 'H2';
            checks.push({
                key: 'heading_hierarchy',
                label: 'Baslik Hiyerarsisi',
                status: 'warning',
                message: `Icerikte yalnizca ${found} basliklari var. Daha iyi bir hiyerarsi icin ${missing} basliklari da ekleyin.`,
                points: 5,
            });
        } else {
            checks.push({
                key: 'heading_hierarchy',
                label: 'Baslik Hiyerarsisi',
                status: 'error',
                message: 'Icerikte H2/H3 baslik bulunamadi. Icerik yapisini iyilestirmek icin alt basliklar ekleyin.',
                points: 0,
            });
        }
    }

    /** Check 12: keyword_in_first_paragraph (max 9 pts) */
    _checkKeywordInFirstParagraph(checks) {
        const keyword = this._keyword;
        const content = this._content;

        if (!keyword) {
            checks.push({
                key: 'keyword_in_first_paragraph',
                label: 'Ilk Paragrafta Anahtar Kelime',
                status: 'error',
                message: 'Odak anahtar kelime belirlenmemis.',
                points: 0,
            });
            return;
        }

        if (!content) {
            checks.push({
                key: 'keyword_in_first_paragraph',
                label: 'Ilk Paragrafta Anahtar Kelime',
                status: 'error',
                message: 'Icerik bulunamadi.',
                points: 0,
            });
            return;
        }

        const plainText = this._stripTags(content);
        const first200 = plainText.substring(0, 200);
        const found = first200.toLowerCase().includes(keyword.toLowerCase());

        checks.push({
            key: 'keyword_in_first_paragraph',
            label: 'Ilk Paragrafta Anahtar Kelime',
            status: found ? 'success' : 'error',
            message: found
                ? 'Odak anahtar kelime icerigin ilk 200 karakterinde bulundu.'
                : 'Odak anahtar kelime icerigin basinda bulunamadi. Anahtar kelimeyi ilk paragrafa ekleyin.',
            points: found ? 9 : 0,
        });
    }

    // ---------------------------------------------------------------
    // Rendering
    // ---------------------------------------------------------------

    _renderScoreBar(score) {
        if (this.hasScoreBarTarget) {
            const percentage = Math.min(100, Math.max(0, score));
            this.scoreBarTarget.style.width = `${percentage}%`;

            // Color coding
            if (score >= 70) {
                this.scoreBarTarget.style.backgroundColor = '#28a745'; // green
                this.scoreBarTarget.className = this.scoreBarTarget.className.replace(
                    /bg-\w+/g,
                    'bg-success',
                );
            } else if (score >= 40) {
                this.scoreBarTarget.style.backgroundColor = '#ffc107'; // yellow
                this.scoreBarTarget.className = this.scoreBarTarget.className.replace(
                    /bg-\w+/g,
                    'bg-warning',
                );
            } else {
                this.scoreBarTarget.style.backgroundColor = '#dc3545'; // red
                this.scoreBarTarget.className = this.scoreBarTarget.className.replace(
                    /bg-\w+/g,
                    'bg-danger',
                );
            }
        }

        if (this.hasScoreValueTarget) {
            this.scoreValueTarget.textContent = score;
        }
    }

    _renderGrade(grade) {
        if (this.hasGradeValueTarget) {
            this.gradeValueTarget.textContent = grade;

            // Apply color class
            const colorMap = { A: 'text-success', B: 'text-info', C: 'text-warning', D: 'text-danger', F: 'text-danger' };
            this.gradeValueTarget.className = this.gradeValueTarget.className.replace(
                /text-(success|info|warning|danger)/g,
                '',
            );
            this.gradeValueTarget.classList.add(colorMap[grade] || 'text-danger');
        }
    }

    _renderChecks(checks) {
        if (!this.hasChecksListTarget) return;

        const iconMap = {
            success: '<i class="fas fa-check-circle text-success me-2"></i>',
            warning: '<i class="fas fa-exclamation-triangle text-warning me-2"></i>',
            error: '<i class="fas fa-times-circle text-danger me-2"></i>',
        };

        const html = checks
            .map((check) => {
                const icon = iconMap[check.status] || iconMap.error;
                return `
                    <li class="list-group-item d-flex align-items-start py-2 px-3 seo-check-item seo-check-${check.status}">
                        <span class="seo-check-icon mt-1">${icon}</span>
                        <div class="flex-grow-1">
                            <strong class="d-block small">${check.label}</strong>
                            <span class="d-block small text-muted">${check.message}</span>
                        </div>
                        <span class="badge ${check.status === 'success' ? 'bg-success' : check.status === 'warning' ? 'bg-warning text-dark' : 'bg-danger'} ms-2 align-self-center">${check.points} puan</span>
                    </li>`;
            })
            .join('');

        this.checksListTarget.innerHTML = html;
    }

    _renderSerpPreview() {
        if (!this.hasSerpPreviewTarget) return;

        const title = this._title || 'Sayfa Basligi';
        const description = this._description || 'Meta aciklama buraya gelecek...';
        const slug = this._slug || 'sayfa-url';
        const siteUrl = this.siteUrlValue;

        // Truncate for SERP display
        const serpTitle = title.length > 60 ? title.substring(0, 57) + '...' : title;
        const serpDesc = description.length > 160 ? description.substring(0, 157) + '...' : description;

        this.serpPreviewTarget.innerHTML = `
            <div class="serp-preview-container p-3 border rounded bg-white">
                <div class="serp-title" style="color: #1a0dab; font-size: 18px; line-height: 1.3; font-family: arial, sans-serif; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${this._escapeHtml(serpTitle)}</div>
                <div class="serp-url" style="color: #006621; font-size: 14px; line-height: 1.4; font-family: arial, sans-serif; margin-top: 1px;">${this._escapeHtml(siteUrl)}/${this._escapeHtml(slug)}</div>
                <div class="serp-description" style="color: #545454; font-size: 13px; line-height: 1.57; font-family: arial, sans-serif; margin-top: 1px; word-wrap: break-word;">${this._escapeHtml(serpDesc)}</div>
            </div>`;
    }

    _escapeHtml(text) {
        const div = document.createElement('div');
        div.appendChild(document.createTextNode(text));
        return div.innerHTML;
    }
}
