/**
 * Smart Builder Module
 * Handles: Schema type selection, field rendering, schema generation
 */
(function($) {
    'use strict';

    // Debounce utility
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func.apply(this, args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    // i18n helper — resolves a translated string from schemafy_ajax.i18n or falls back to the default
    function t(key, fallback) {
        return (typeof schemafy_ajax !== 'undefined' && schemafy_ajax.i18n && schemafy_ajax.i18n[key])
            ? schemafy_ajax.i18n[key]
            : fallback;
    }

    window.SchemafyBuilder = {
        // Flag to prevent duplicate saves
        isSavingSchema: false,

        /**
         * Map builder card types to correct Schema.org types
         * Ensures generated JSON uses the correct @type property
         */
        cardTypeToSchemaType: {
            'FAQ': 'FAQPage',
            'Video': 'VideoObject'
        },

        /**
         * Schema.org subtypes that should use a parent builder routine while
         * preserving their original @type in the generated JSON.
         */
        schemaTypeBuildMap: {
            'FAQPage': 'FAQ',
            'VideoObject': 'VideoObject',
            'NewsArticle': 'Article', 'BlogPosting': 'Article',
            'AdvertiserContentArticle': 'Article', 'AnalysisNewsArticle': 'Article',
            'AskPublicNewsArticle': 'Article', 'BackgroundNewsArticle': 'Article',
            'MedicalScholarlyArticle': 'Article', 'ReportageNewsArticle': 'Article',
            'SatiricalArticle': 'Article', 'ScholarlyArticle': 'Article',
            'SocialMediaPosting': 'Article', 'TechArticle': 'Article',
            'AccountingService': 'LocalBusiness', 'AutoDealer': 'LocalBusiness',
            'AutoRental': 'LocalBusiness', 'AutoRepair': 'LocalBusiness',
            'AutoWash': 'LocalBusiness', 'AutomotiveBusiness': 'LocalBusiness',
            'Bakery': 'LocalBusiness', 'BeautySalon': 'LocalBusiness',
            'CafeOrCoffeeShop': 'LocalBusiness', 'ChildCare': 'LocalBusiness',
            'Dentist': 'LocalBusiness', 'Electrician': 'LocalBusiness',
            'EntertainmentBusiness': 'LocalBusiness', 'EventVenue': 'LocalBusiness',
            'ExerciseGym': 'LocalBusiness', 'FinancialService': 'LocalBusiness',
            'Florist': 'LocalBusiness', 'FoodEstablishment': 'LocalBusiness',
            'GeneralContractor': 'LocalBusiness', 'GovernmentOffice': 'LocalBusiness',
            'HealthAndBeautyBusiness': 'LocalBusiness', 'HomeAndConstructionBusiness': 'LocalBusiness',
            'HousePainter': 'LocalBusiness', 'HVACBusiness': 'LocalBusiness',
            'InsuranceAgency': 'LocalBusiness', 'LegalService': 'LocalBusiness',
            'Locksmith': 'LocalBusiness', 'LodgingBusiness': 'LocalBusiness',
            'MedicalBusiness': 'LocalBusiness', 'MovingCompany': 'LocalBusiness',
            'NailSalon': 'LocalBusiness', 'Optician': 'LocalBusiness',
            'Pharmacy': 'LocalBusiness', 'Physician': 'LocalBusiness',
            'Physiotherapist': 'LocalBusiness', 'Plumber': 'LocalBusiness',
            'ProfessionalService': 'LocalBusiness', 'RealEstateAgent': 'LocalBusiness',
            'Restaurant': 'LocalBusiness', 'RoofingContractor': 'LocalBusiness',
            'SportsActivityLocation': 'LocalBusiness', 'Store': 'LocalBusiness',
            'TaxiService': 'LocalBusiness', 'TravelAgency': 'LocalBusiness',
            'VeterinaryCare': 'LocalBusiness',
            'Airline': 'Organization', 'Consortium': 'Organization',
            'Corporation': 'Organization', 'EducationalOrganization': 'Organization',
            'GovernmentOrganization': 'Organization', 'LibrarySystem': 'Organization',
            'NGO': 'Organization', 'NewsMediaOrganization': 'Organization',
            'OnlineBusiness': 'Organization', 'PerformingGroup': 'Organization',
            'Project': 'Organization', 'ResearchOrganization': 'Organization',
            'SearchRescueOrganization': 'Organization', 'SportsOrganization': 'Organization',
            'WorkersUnion': 'Organization',
            'AboutPage': 'WebPage', 'CheckoutPage': 'WebPage',
            'ContactPage': 'WebPage', 'WebSite': 'WebPage',
            'CollectionPage': 'WebPage', 'ImageGallery': 'WebPage',
            'ItemPage': 'WebPage', 'MedicalWebPage': 'WebPage',
            'ProfilePage': 'WebPage', 'QAPage': 'WebPage',
            'RealEstateListing': 'WebPage', 'SearchResultsPage': 'WebPage'
        },

        getSchemaTypeRegistry: function() {
            if (typeof schemafy_ajax !== 'undefined' && schemafy_ajax.schema_type_registry) {
                return schemafy_ajax.schema_type_registry;
            }

            if (typeof schemafyBuilder !== 'undefined' && schemafyBuilder.schema_type_registry) {
                return schemafyBuilder.schema_type_registry;
            }

            return {};
        },

        normalizeSchemaType: function(type) {
            if (!type) return '';

            return String(type).trim().replace(/^https?:\/\/schema\.org\//i, '');
        },

        getSchemaBuildType: function(type) {
            const normalizedType = this.normalizeSchemaType(type);
            const registry = this.getSchemaTypeRegistry();
            const buildMap = registry.buildMap || {};

            return buildMap[normalizedType] || this.schemaTypeBuildMap[normalizedType] || '';
        },

        resolveSchemaBuildType: function(schemaType, formType, buildHint) {
            const normalizedHint = this.normalizeSchemaType(buildHint);

            if (normalizedHint) {
                const hintBuildType = this.getSchemaBuildType(normalizedHint);
                if (hintBuildType) {
                    return hintBuildType;
                }

                const hintSchemaType = this.cardTypeToSchemaType[normalizedHint];
                if (hintSchemaType) {
                    return this.getSchemaBuildType(hintSchemaType) || hintSchemaType;
                }

                return normalizedHint;
            }

            return this.getSchemaBuildType(schemaType) ||
                this.getSchemaBuildType(formType) ||
                this.cardTypeToSchemaType[formType] ||
                formType;
        },

        /**
         * Handle schema type card selection
         */
        selectSchemaType: function(e) {
            const $btn = $(e.currentTarget);

            const type = $btn.data('type');

            // Check if we're currently in editing mode
            const $form = $('#schemafy-dynamic-schema-form');
            const editingId = $form.data('editing-id');

            if (editingId) {
                // Reset editing state when selecting a different schema type
                $form.removeData('editing-id');
                $form.removeData('original-schema-data original-schema-type storage-schema-type schema-builder-type');

                // Reset save button and remove editing indicator
                if (window.SchemafyOperations && typeof window.SchemafyOperations.resetSaveButton === 'function') {
                    window.SchemafyOperations.resetSaveButton();
                }
            }

            $('.schemafy-schema-type-card').removeClass('active');
            $btn.addClass('active');

            this.updateStepIndicator(2);
            this.loadSchemaFields(type);
        },

        /**
         * Filter sidebar schema type cards by category pill
         */
        selectCategory: function(cat) {
            $('.schemafy-category-btn').removeClass('active');
            $('.schemafy-category-btn[data-category="' + cat + '"]').addClass('active');

            if (cat === 'all') {
                $('.schemafy-schema-type-card').removeClass('schemafy-hidden-by-filter');
            } else {
                $('.schemafy-schema-type-card').each(function() {
                    const cardCat = $(this).data('category');
                    $(this).toggleClass('schemafy-hidden-by-filter', cardCat !== cat);
                });
            }
        },

        /**
         * Advance or update the step indicator bar (1 = Choose, 2 = Fill, 3 = Apply)
         */
        updateStepIndicator: function(activeStep) {
            $('#schemafy-step-indicator .schemafy-step-item').each(function() {
                const step = parseInt($(this).data('step'), 10);
                $(this).removeClass('active done');
                if (step < activeStep) {
                    $(this).addClass('done');
                } else if (step === activeStep) {
                    $(this).addClass('active');
                }
            });

            $('#schemafy-step-indicator .schemafy-step-connector').each(function(i) {
                $(this).toggleClass('done', (i + 1) < activeStep);
            });
        },

        /**
         * Toggle the raw JSON preview panel open/closed.
         * Uses jQuery show/hide directly — never relies on CSS classes for visibility
         * so it works regardless of stylesheet load order.
         */
        toggleJsonPreview: function() {
            const $body  = $('#schemafy-preview-body');
            const $btn   = $('#schemafy-toggle-json-preview');
            const $arrow = $btn.find('.schemafy-toggle-arrow');

            if ($body.is(':visible')) {
                $body.slideUp(180);
                $arrow.text('▼');
                $btn.attr('aria-expanded', 'false');
            } else {
                $body.slideDown(180);
                $arrow.text('▲');
                $btn.attr('aria-expanded', 'true');
            }
        },

        /**
         * Update the friendly preview status card (type / page / fields filled)
         */
        updateFriendlyPreview: function() {
            const $form = $('#schemafy-dynamic-schema-form');
            const $hint = $('.schemafy-preview-friendly-hint');
            const $rows = $('#schemafy-friendly-rows');

            if ($form.length === 0) {
                $hint.show();
                $rows.hide();
                return;
            }

            const type = $form.find('[name="schema_type"]').val();
            if (!type) {
                $hint.show();
                $rows.hide();
                return;
            }

            // Count visible, non-hidden, non-empty fields
            let filled = 0, total = 0;
            $form.find('input:not([type="hidden"]):not([name="_target_urls"]), textarea, select').each(function() {
                total++;
                const val = $(this).val();
                if (val && val.trim() !== '') filled++;
            });

            const urls = this.getSelectedUrls();
            let urlText = '–';
            if (urls.length > 0) {
                urlText = urls[0].replace(/^https?:\/\//, '').replace(/\/$/, '');
                if (urlText.length > 28) urlText = urlText.substring(0, 26) + '…';
            }

            const typeLabels = {
                'Article': '📰 Article', 'Product': '🛍️ Product', 'Organization': '🏢 Organization',
                'LocalBusiness': '🏪 Local Business', 'Person': '👤 Person', 'Event': '🎉 Event',
                'Recipe': '🍳 Recipe', 'Review': '⭐ Review', 'FAQ': '❓ FAQ',
                'WebPage': '📄 Web Page', 'Course': '🎓 Course', 'HowTo': '🔧 How-To',
                'JobPosting': '💼 Job Posting', 'Service': '🛠️ Service',
                'Video': '🎬 Video', 'BreadcrumbList': '🍞 Breadcrumbs'
            };

            $('#schemafy-friendly-type').text(typeLabels[type] || type);
            $('#schemafy-friendly-url').text(urlText);
            $('#schemafy-friendly-fields').text(filled + ' / ' + total);

            $hint.hide();
            $rows.show();
        },

        /**
         * Load schema fields via AJAX
         */
        loadSchemaFields: function(type) {
            const $container = $('#schemafy-builder-form-container');
            $container.html('<div class="schemafy-loading-v2"><div class="schemafy-spinner-v2"></div><p>' + t('loading_fields', 'Loading fields...') + '</p></div>');

            $.ajax({
                url: schemafy_ajax.ajax_url,
                type: 'POST',
                data: {
                    action: 'schemafy_get_schema_fields',
                    type: type,
                    nonce: schemafy_ajax.nonce
                },
                success: (response) => {
                    if (response.success) {
                        if (response.data.template_available && response.data.config) {
                            this.renderSchemaFieldsFromConfig(type, response.data.config);
                        } else {
                            this.renderSchemaFields(type, response.data);
                        }
                    }
                },
                error: () => {
                    $container.html('<p>' + t('error_loading_fields', 'Error loading fields') + '</p>');
                }
            });
        },

        /**
         * Render schema fields from template
         */
        renderSchemaFields: function(type, fields) {
            const $container = $('#schemafy-builder-form-container');

            let html = `
                <form id="schemafy-dynamic-schema-form" class="schemafy-form-v2">
                    <input type="hidden" name="schema_type" value="${type}">

                    <div class="schemafy-form-group-v2">
                        <label>${t('target_urls_label', 'Target URLs')} <span class="schemafy-required">*</span></label>
                        <div class="schemafy-url-dropdown-container">
                            <div class="schemafy-url-combobox-wrap">
                                <input type="text" id="schemafy-url-combobox" class="schemafy-url-combobox"
                                       placeholder="Search pages or paste a URL..."
                                       autocomplete="off" spellcheck="false">
                            </div>
                            <div class="schemafy-url-results" id="schemafy-url-results"></div>
                            <div class="schemafy-selected-urls" id="schemafy-selected-urls"></div>
                            <input type="hidden" name="_target_urls" id="schemafy-target-urls" value="[]">
                        </div>
                    </div>

                    <div class="schemafy-form-group-v2">
                        <label>${t('schema_name_label', 'Give it a nickname')} <span class="schemafy-required">*</span>
                            <span class="schemafy-field-help" style="font-weight:400;font-size:11px;margin-left:4px;">${t('schema_name_hint', '(only you see this)')}</span>
                        </label>
                        <input type="text" name="schema_name" required placeholder="e.g., My Homepage ${type}, Product star...">

                    <hr style="margin: 24px 0; border: none; border-top: 1px solid var(--schemafy-gray-200);">

                    <h3 style="margin-bottom: 16px; font-size: 16px;">${type} ${t('fields_heading', 'Fields')}</h3>
            `;

            const fieldTemplates = this.getFieldTemplates(type);
            fieldTemplates.forEach(field => {
                html += this.renderField(field);
            });

            html += '</form>';
            $container.html(html);

            this.initUrlDropdown();
            this.initVariableSystem();

            const debouncedUpdate = debounce(() => {
                this.updateBuilderPreview();
            }, 300);

            // Use event delegation so dynamically-added array items also trigger preview updates
            $(document).off('input.bsbuilder change.bsbuilder')
                .on('input.bsbuilder change.bsbuilder',
                    '#schemafy-dynamic-schema-form input, #schemafy-dynamic-schema-form textarea, #schemafy-dynamic-schema-form select',
                    debouncedUpdate);
            this.updateBuilderPreview();

            // Notify any pending schema-edit operation that the form is ready
            if (typeof window._schemafyBuilderFormReady === 'function') {
                window._schemafyBuilderFormReady(type);
            }
        },

        /**
         * Render fields from JSON config
         */
        renderSchemaFieldsFromConfig: function(type, config) {
            const $container = $('#schemafy-builder-form-container');

            let html = `
                <form id="schemafy-dynamic-schema-form" class="schemafy-form-v2">
                    <input type="hidden" name="schema_type" value="${type}">

                    <div class="schemafy-form-group-v2">
                        <label>${t('target_urls_label', 'Target URLs')} <span class="schemafy-required">*</span></label>
                        <div class="schemafy-url-dropdown-container">
                            <div class="schemafy-url-combobox-wrap">
                                <input type="text" id="schemafy-url-combobox" class="schemafy-url-combobox"
                                       placeholder="Search pages or paste a URL..."
                                       autocomplete="off" spellcheck="false">
                            </div>
                            <div class="schemafy-url-results" id="schemafy-url-results"></div>
                            <div class="schemafy-selected-urls" id="schemafy-selected-urls"></div>
                            <input type="hidden" name="_target_urls" id="schemafy-target-urls" value="[]">
                        </div>
                    </div>

                    <div class="schemafy-form-group-v2">
                        <label>${t('schema_name_label', 'Schema Name')} <span class="schemafy-required">*</span></label>
                        <input type="text" name="schema_name" required placeholder="e.g., My ${type} schema...">
                    </div>

                    <hr style="margin: 24px 0; border: none; border-top: 1px solid var(--schemafy-gray-200);">

                    <h3 style="margin-bottom: 16px; font-size: 16px;">${config.icon || ''} ${type} ${t('fields_heading', 'Fields')}</h3>
            `;

            if (config.description) {
                html += `<p style="color: #666; margin-bottom: 20px; font-size: 14px;">${config.description}</p>`;
            }

            if (config.fields && Array.isArray(config.fields)) {
                config.fields.forEach(field => {
                    html += this.renderField(field);
                });
            }

            html += '</form>';
            $container.html(html);

            this.initUrlDropdown();
            this.initVariableSystem();

            const debouncedUpdate = debounce(() => {
                this.updateBuilderPreview();
            }, 300);

            // Use event delegation so dynamically-added array items also trigger preview updates
            $(document).off('input.bsbuilder change.bsbuilder')
                .on('input.bsbuilder change.bsbuilder',
                    '#schemafy-dynamic-schema-form input, #schemafy-dynamic-schema-form textarea, #schemafy-dynamic-schema-form select',
                    debouncedUpdate);
            this.updateBuilderPreview();

            // Notify any pending schema-edit operation that the form is ready
            if (typeof window._schemafyBuilderFormReady === 'function') {
                window._schemafyBuilderFormReady(type);
            }
        },

        /**
         * Get field templates for schema type
         */
        getFieldTemplates: function(type) {
            const templates = {
                'Article': [
                    { name: 'headline', label: 'Headline', type: 'text', required: true, placeholder: 'Article title' },
                    { name: 'author', label: 'Author Name', type: 'text', required: true, placeholder: 'John Doe' },
                    { name: 'datePublished', label: 'Date Published', type: 'date', required: true },
                    { name: 'dateModified', label: 'Date Modified', type: 'date' },
                    { name: 'image', label: 'Image URL', type: 'image', placeholder: 'https://example.com/image.jpg' },
                    { name: 'description', label: 'Description', type: 'textarea', placeholder: 'Article summary' }
                ],
                'Product': [
                    { name: 'name', label: 'Product Name', type: 'text', required: true, placeholder: 'Product Name' },
                    { name: 'description', label: 'Description', type: 'textarea', required: true },
                    { name: 'image', label: 'Image URL', type: 'image', placeholder: 'https://example.com/product.jpg' },
                    { name: 'price', label: 'Price', type: 'number', required: true, placeholder: '99.99' },
                    { name: 'currency', label: 'Currency', type: 'text', required: true, placeholder: 'USD' },
                    { name: 'availability', label: 'Availability', type: 'select', options: ['InStock', 'OutOfStock', 'PreOrder'] },
                    { name: 'brand', label: 'Brand', type: 'text', placeholder: 'Brand Name' },
                    { name: 'sku', label: 'SKU', type: 'text', placeholder: 'SKU-123' },
                    { name: 'gtin', label: 'GTIN / Barcode', type: 'text', placeholder: '0123456789012', helpText: 'EAN/UPC barcode: 8, 12, 13 or 14 digits. Recommended by Google for products.' },
                    { name: 'mpn', label: 'MPN', type: 'text', placeholder: 'MPN-001', helpText: 'Manufacturer Part Number' },
                    { name: 'ratingValue', label: 'Rating Value', type: 'number', placeholder: '4.5', helpText: 'Average rating (e.g., 4.5)', min: 0, step: '0.1' },
                    { name: 'reviewCount', label: 'Review Count', type: 'number', placeholder: '100', helpText: 'Number of reviews', min: 0, step: '1' },
                    { name: 'bestRating', label: 'Best Rating', type: 'number', placeholder: '5', helpText: 'Highest possible rating', min: 0, step: '1' },
                    { name: 'worstRating', label: 'Worst Rating', type: 'number', placeholder: '1', helpText: 'Lowest possible rating (0 or 1)', min: 0, step: '1' }
                ],
                'Organization': [
                    { name: 'name', label: 'Organization Name', type: 'text', required: true, placeholder: 'ACME Corp' },
                    { name: 'url', label: 'Website URL', type: 'url', required: true, placeholder: 'https://example.com' },
                    { name: 'logo', label: 'Logo URL', type: 'image', placeholder: 'https://example.com/logo.png' },
                    { name: 'description', label: 'Description', type: 'textarea' },
                    { name: 'telephone', label: 'Phone', type: 'tel', placeholder: '+1-234-567-8900' },
                    { name: 'email', label: 'Email', type: 'email', placeholder: 'info@example.com' }
                ],
                'LocalBusiness': [
                    { name: 'name', label: 'Business Name', type: 'text', required: true },
                    { name: 'address', label: 'Street Address', type: 'text', required: true },
                    { name: 'city', label: 'City', type: 'text', required: true },
                    { name: 'state', label: 'State', type: 'text', required: true },
                    { name: 'postalCode', label: 'Postal Code', type: 'text', required: true },
                    { name: 'telephone', label: 'Phone', type: 'tel', required: true },
                    { name: 'priceRange', label: 'Price Range', type: 'text', placeholder: '$$' }
                ],
                'FAQ': [
                    {
                        name: 'mainEntity',
                        label: 'FAQ Questions & Answers',
                        type: 'array',
                        fields: [
                            { name: 'questionName', label: 'Question', type: 'text', required: true },
                            { name: 'acceptedAnswer', label: 'Answer', type: 'textarea', required: true }
                        ]
                    }
                ],
                'FAQPage': [
                    {
                        name: 'mainEntity',
                        label: 'FAQ Questions & Answers',
                        type: 'array',
                        fields: [
                            { name: 'questionName', label: 'Question', type: 'text', required: true },
                            { name: 'acceptedAnswer', label: 'Answer', type: 'textarea', required: true }
                        ]
                    }
                ]
            };

            return templates[type] || [];
        },

        /**
         * Render a single field
         */
        /**
         * Common ISO 4217 currency codes offered in currency selects.
         */
        currencyCodes: ['USD', 'EUR', 'GBP', 'COP', 'MXN', 'ARS', 'CLP', 'PEN', 'BRL', 'CAD', 'AUD', 'JPY', 'CNY', 'INR', 'CHF', 'SEK', 'NOK', 'DKK', 'PLN', 'UYU', 'BOB', 'CRC', 'DOP', 'GTQ', 'HNL', 'NIO', 'PAB', 'PYG', 'VES'],

        /**
         * Field names that hold ISO 8601 durations (PT1H30M).
         */
        durationFieldNames: ['totalTime', 'prepTime', 'performTime', 'cookTime', 'timeRequired'],

        /**
         * Upgrade generic field definitions to specialized widgets.
         *
         * Free-text inputs for ISO durations ("PT30M") and currency codes
         * ("USD") are unfillable for non-technical users and the #1 source of
         * malformed schemas. Swap them for structured widgets here so both the
         * inline JS templates and the JSON field-configs benefit.
         */
        normalizeFieldDefinition: function(field) {
            const baseName = String(field.name || '').split('[').pop().replace(']', '');

            if ((field.type === 'text' || !field.type) && this.durationFieldNames.indexOf(baseName) !== -1) {
                return Object.assign({}, field, {
                    type: 'duration',
                    helpText: t('duration_widget_help', 'Set hours and minutes — the format Google requires is generated automatically.'),
                });
            }

            if ((field.type === 'text' || !field.type) && /currency$/i.test(baseName)) {
                return Object.assign({}, field, { type: 'currency' });
            }

            if (field.type === 'tel' && !field.helpText) {
                return Object.assign({}, field, {
                    helpText: t('phone_format_hint', 'International format recommended: +57 601 1234567'),
                });
            }

            return field;
        },

        renderField: function(field) {
            field = this.normalizeFieldDefinition(field);
            const isSection = field.type === 'array' || field.type === 'group';
            let html = `<div class="schemafy-form-group-v2${isSection ? ' schemafy-form-group-section' : ''}">`;
            html += `<label>${field.label}`;
            if (field.required) html += ' <span class="schemafy-required">*</span>';
            html += `</label>`;

            if (field.helpText) {
                html += `<small style="display: block; color: #666; margin-bottom: 6px;">${field.helpText}</small>`;
            }

            switch(field.type) {
                case 'textarea':
                    const rows = field.rows || 3;
                    const textareaId = `field-${field.name}-${Date.now()}`;
                    html += `<div style="position: relative;">`;
                    // padding-top reserves a strip for the Variables button so it
                    // never covers the first line of text (recurring reported bug).
                    html += `<textarea id="${textareaId}" name="${field.name}" ${field.required ? 'required' : ''} placeholder="${field.placeholder || ''}" rows="${rows}" style="padding-top: 38px;"`;
                    if (field.maxLength) html += ` maxlength="${field.maxLength}"`;
                    html += `></textarea>`;
                    html += `<button type="button" class="schemafy-insert-variable-btn" data-target="${textareaId}" title="${t('insert_variable_title', 'Insert Variable')}" style="position: absolute; right: 8px; top: 6px; background: var(--schemafy-primary); color: white; border: none; border-radius: 4px; padding: 4px 8px; cursor: pointer; font-size: 11px; z-index: 10;">
                        <span class="dashicons dashicons-admin-generic" style="font-size: 14px; width: 14px; height: 14px; line-height: 14px;"></span> ${t('variables_btn', 'Variables')}
                    </button>`;
                    html += `</div>`;
                    break;
                case 'select':
                    html += `<select name="${field.name}" ${field.required ? 'required' : ''}>`;
                    html += `<option value="">${t('select_placeholder', 'Select...')}</option>`;
                    if (field.options) {
                        field.options.forEach(opt => {
                            // Support both simple strings and {value, label} objects
                            if (typeof opt === 'object' && opt.value !== undefined) {
                                html += `<option value="${opt.value}">${opt.label || opt.value}</option>`;
                            } else {
                                html += `<option value="${opt}">${opt}</option>`;
                            }
                        });
                    }
                    html += `</select>`;
                    break;
                case 'image':
                    // Image field with WordPress Media Library selector
                    const imageInputId = `field-${field.name}-${Date.now()}`;
                    html += `<div class="schemafy-image-field-wrapper" style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap;">`;
                    html += `<input type="hidden" id="${imageInputId}" name="${field.name}" ${field.required ? 'required' : ''} class="schemafy-image-url-field">`;
                    html += `<button type="button" class="schemafy-btn-secondary schemafy-select-media" data-target="${imageInputId}" style="white-space: nowrap; flex-shrink: 0;">`;
                    html += `<span class="dashicons dashicons-admin-media" style="vertical-align: middle;"></span> ${t('select_image_title', 'Select Image')}`;
                    html += `</button>`;
                    html += `<span class="schemafy-image-url-text" style="font-size:12px;color:var(--schemafy-gray-500);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;">${t('no_image_selected', 'No image selected')}</span>`;
                    html += `</div>`;
                    html += `<div id="${imageInputId}-preview" class="schemafy-image-preview" style="margin-top: 8px; display: none;">`;
                    html += `<img src="" alt="Preview" style="max-width: 200px; max-height: 150px; border-radius: 4px; border: 1px solid #ddd;">`;
                    html += `<button type="button" class="schemafy-remove-image" data-target="${imageInputId}" style="margin-left: 8px; color: #dc3545; background: none; border: none; cursor: pointer;">`;
                    html += `<span class="dashicons dashicons-no-alt"></span> ${t('remove_item', 'Remove')}`;
                    html += `</button>`;
                    html += `</div>`;
                    break;
                case 'array':
                    html += `<div class="schemafy-field-array" data-field-name="${field.name}" style="margin-top: 8px;">`;
                    if (field.presets && field.presets.length > 0) {
                        html += `<div class="schemafy-array-presets" style="margin-bottom: 10px; display: flex; flex-wrap: wrap; gap: 6px;">`;
                        field.presets.forEach(preset => {
                            const presetsJson = JSON.stringify(preset.items).replace(/'/g, '&#39;');
                            html += `<button type="button" class="schemafy-btn-secondary schemafy-array-preset-btn" data-field-name="${field.name}" data-fields='${JSON.stringify(field.fields)}' data-preset='${presetsJson}' style="font-size: 12px; padding: 4px 10px;">`;
                            if (preset.icon) html += `${preset.icon} `;
                            html += `${preset.label}</button>`;
                        });
                        html += `</div>`;
                    }
                    html += `<div class="schemafy-array-items" id="schemafy-array-${field.name}">`;
                    html += this.renderArrayItem(field, 0);
                    html += `</div>`;
                    html += `<button type="button" class="schemafy-btn-secondary schemafy-add-array-item" data-field-name="${field.name}" data-fields='${JSON.stringify(field.fields)}' style="margin-top: 12px;">`;
                    html += `<span class="dashicons dashicons-plus-alt" style="vertical-align: middle;"></span> ${t('add_another', 'Add Another')} ${field.label.replace(/s$/, '')}`;
                    html += `</button>`;
                    html += `</div>`;
                    return html;
                case 'group':
                    // Single-sub-field group: collapse to avoid double-label (outer + inner).
                    // Use the group's label/helpText for the sub-field so the section label
                    // is still shown, but without a redundant nested label below it.
                    if (field.fields && field.fields.length === 1) {
                        const only = { ...field.fields[0] };
                        only.name = `${field.name}.${only.name}`;
                        only.label = field.label;
                        only.helpText = field.helpText || only.helpText;
                        only.required = field.required || only.required;
                        return this.renderField(only);
                    }
                    html += `<div class="schemafy-field-group" style="border-left: 3px solid var(--schemafy-primary); padding-left: 16px; margin-top: 8px; margin-bottom: 16px;">`;
                    if (field.fields && Array.isArray(field.fields)) {
                        field.fields.forEach(subField => {
                            const prefixedSubField = { ...subField };
                            prefixedSubField.name = `${field.name}.${subField.name}`;
                            html += this.renderField(prefixedSubField);
                        });
                    }
                    html += `</div>`;
                    return html;
                case 'datetime-local':
                case 'datetime':
                case 'date':
                case 'time':
                    // Date/time fields with native picker only (no manual text input)
                    const dateInputId = `field-${field.name}-${Date.now()}`;
                    // Convert 'datetime' to 'date' (HTML5 doesn't have 'datetime' type)
                    const inputType = field.type === 'datetime' ? 'date' : field.type;
                    html += `<div class="schemafy-date-field-wrapper" style="position: relative;">`;
                    html += `<input type="${inputType}" id="${dateInputId}" name="${field.name}" ${field.required ? 'required' : ''}`;
                    if (field.min !== undefined && field.min !== null) html += ` min="${field.min}"`;
                    if (field.max !== undefined && field.max !== null) html += ` max="${field.max}"`;
                    html += ` class="schemafy-date-input schemafy-date-picker-only" style="cursor: pointer;" onkeydown="return false;">`;
                    html += `</div>`;
                    if (inputType === 'date') {
                        html += `<small style="display: block; color: #666; margin-top: 4px;"><span class="dashicons dashicons-calendar-alt" style="font-size: 14px; vertical-align: middle;"></span> ${t('click_to_select_date', 'Click to select date using the date picker')}</small>`;
                    }
                    break;
                case 'duration':
                    // Structured hours/minutes widget that emits ISO 8601 (PT1H30M)
                    // into a hidden input. Users never type the PT format by hand.
                    html += `<div class="schemafy-duration-widget" style="display: flex; align-items: center; gap: 8px;">`;
                    html += `<input type="number" class="schemafy-duration-hours" min="0" step="1" placeholder="0" style="width: 80px;" aria-label="${t('hours_label', 'Hours')}">`;
                    html += `<span style="color: #666; font-size: 13px;">${t('hours_short', 'h')}</span>`;
                    html += `<input type="number" class="schemafy-duration-minutes" min="0" max="59" step="1" placeholder="30" style="width: 80px;" aria-label="${t('minutes_label', 'Minutes')}">`;
                    html += `<span style="color: #666; font-size: 13px;">${t('minutes_short', 'min')}</span>`;
                    html += `<input type="hidden" name="${field.name}" class="schemafy-duration-value" ${field.required ? 'required' : ''}>`;
                    html += `</div>`;
                    break;
                case 'currency':
                    // ISO 4217 select — free text here produced "$"/"pesos" in priceCurrency
                    html += `<select name="${field.name}" ${field.required ? 'required' : ''}>`;
                    html += `<option value="">${t('select_currency', '— Select currency —')}</option>`;
                    this.currencyCodes.forEach(code => {
                        html += `<option value="${code}">${code}</option>`;
                    });
                    html += `</select>`;
                    break;
                default:
                    // For text/url/email fields, add variable button
                    if (field.type === 'text' || field.type === 'url' || field.type === 'email') {
                        const inputId = `field-${field.name}-${Date.now()}`;
                        html += `<div style="position: relative;">`;
                        html += `<input id="${inputId}" type="${field.type}" name="${field.name}" ${field.required ? 'required' : ''} placeholder="${field.placeholder || ''}"`;
                        if (field.min !== undefined && field.min !== null) html += ` min="${field.min}"`;
                        if (field.max !== undefined && field.max !== null) html += ` max="${field.max}"`;
                        if (field.maxLength) html += ` maxlength="${field.maxLength}"`;
                        html += ` style="padding-right: 90px;">`;
                        html += `<button type="button" class="schemafy-insert-variable-btn" data-target="${inputId}" title="${t('insert_variable_title', 'Insert Variable')}" style="position: absolute; right: 4px; top: 50%; transform: translateY(-50%); background: var(--schemafy-primary); color: white; border: none; border-radius: 4px; padding: 4px 8px; cursor: pointer; font-size: 11px;">
                            <span class="dashicons dashicons-admin-generic" style="font-size: 14px; width: 14px; height: 14px; line-height: 14px;"></span> ${t('variables_btn', 'Variables')}
                        </button>`;
                        html += `</div>`;
                    } else {
                        html += `<input type="${field.type}" name="${field.name}" ${field.required ? 'required' : ''} placeholder="${field.placeholder || ''}"`;
                        if (field.min !== undefined && field.min !== null) html += ` min="${field.min}"`;
                        if (field.max !== undefined && field.max !== null) html += ` max="${field.max}"`;
                        if (field.maxLength) html += ` maxlength="${field.maxLength}"`;
                        if (field.type === 'number') html += ` step="${field.step !== undefined ? field.step : 'any'}"`;
                        html += `>`;
                    }
            }

            html += `</div>`;
            return html;
        },

        /**
         * Render array item for repeatable fields
         */
        renderArrayItem: function(field, index) {
            let html = `<div class="schemafy-array-item" data-index="${index}" style="position: relative; background: var(--schemafy-gray-50); padding: 16px; margin-bottom: 12px; border-radius: var(--schemafy-radius); border: 1px solid var(--schemafy-gray-200);">`;

            if (index > 0) {
                html += `<button type="button" class="schemafy-remove-array-item" style="position: absolute; top: 8px; right: 8px; background: var(--schemafy-danger); color: white; border: none; border-radius: 50%; width: 28px; height: 28px; cursor: pointer;">×</button>`;
            }

            html += `<div style="margin-right: 36px;">`;

            if (field.fields && Array.isArray(field.fields)) {
                field.fields.forEach(subField => {
                    const arraySubField = { ...subField };
                    arraySubField.name = `${field.name}[${index}][${subField.name}]`;
                    html += this.renderField(arraySubField);
                });
            }

            html += `</div></div>`;
            return html;
        },

        /**
         * Initialize array field handlers
         */
        initArrayFieldHandlers: function() {
            const self = this;

            $(document).on('click', '.schemafy-add-array-item', function(e) {
                e.preventDefault();
                const $btn = $(this);
                const fieldName = $btn.data('field-name');
                const fields = $btn.data('fields');
                const $container = $(`#schemafy-array-${fieldName}`);
                const currentCount = $container.find('.schemafy-array-item').length;

                const field = { name: fieldName, fields: fields };
                const newItemHtml = self.renderArrayItem(field, currentCount);
                $container.append(newItemHtml);
                self.updateBuilderPreview();
            });

            $(document).on('click', '.schemafy-remove-array-item', function(e) {
                e.preventDefault();
                $(this).closest('.schemafy-array-item').fadeOut(200, function() {
                    $(this).remove();
                    self.updateBuilderPreview();
                });
            });

            $(document).on('click', '.schemafy-array-preset-btn', function(e) {
                e.preventDefault();
                const $btn = $(this);
                const fieldName = $btn.data('field-name');
                const fields = $btn.data('fields');
                const presetItems = $btn.data('preset');
                const $container = $(`#schemafy-array-${fieldName}`);

                $container.empty();
                presetItems.forEach(function(itemData, idx) {
                    const field = { name: fieldName, fields: fields };
                    const itemHtml = self.renderArrayItem(field, idx);
                    const $item = $(itemHtml);
                    // Populate each sub-field with preset values
                    Object.keys(itemData).forEach(function(key) {
                        $item.find(`[name="${fieldName}[${idx}][${key}]"]`).val(itemData[key]);
                    });
                    $container.append($item);
                });
                self.updateBuilderPreview();
            });

            // Initialize variable system
            this.initVariableSystem();

            // Initialize WordPress Media Library selector
            this.initMediaSelector();

            // Initialize date picker protection (no manual typing)
            this.initDatePickerProtection();

            // Initialize numeric input protection (no commas/thousands separators)
            this.initNumericInputProtection();

            // Initialize duration widgets (hours/minutes → ISO 8601)
            this.initDurationWidgets();
        },

        /**
         * Initialize WordPress Media Library selector for image fields
         */
        initMediaSelector: function() {
            const self = this;
            let mediaFrame = null;
            let currentTarget = null;

            // Handle media select button click
            $(document).on('click', '.schemafy-select-media', function(e) {
                e.preventDefault();

                const targetId = $(this).data('target');
                // Use getElementById to avoid jQuery treating dots in the ID as class selectors
                // (e.g., field-publisher.logo-123 would fail with $('#field-publisher.logo-123'))
                currentTarget = $(document.getElementById(targetId));

                // If media frame exists, open it
                if (mediaFrame) {
                    mediaFrame.open();
                    return;
                }

                // Create WordPress media frame
                mediaFrame = wp.media({
                    title: t('select_image_title', 'Select Image'),
                    button: {
                        text: t('use_this_image', 'Use this image')
                    },
                    multiple: false,
                    library: {
                        type: 'image'
                    }
                });

                // Handle image selection
                mediaFrame.on('select', function() {
                    const attachment = mediaFrame.state().get('selection').first().toJSON();
                    const imageUrl = attachment.url;

                    if (currentTarget && currentTarget.length) {
                        currentTarget.val(imageUrl).trigger('input');

                        // Update the text display span
                        currentTarget.closest('.schemafy-image-field-wrapper').find('.schemafy-image-url-text').text(imageUrl);

                        // Show preview — use getElementById to avoid dot-in-ID selector issue
                        const previewId = currentTarget.attr('id') + '-preview';
                        const $preview = $(document.getElementById(previewId));
                        if ($preview.length) {
                            $preview.find('img').attr('src', imageUrl);
                            $preview.show();
                        }

                        self.updateBuilderPreview();

                        if (window.SchemafyUtilities) {
                            window.SchemafyUtilities.showNotification(t('image_selected', 'Image selected successfully'), 'success');
                        }
                    }
                });

                mediaFrame.open();
            });


            // Handle remove image button
            $(document).on('click', '.schemafy-remove-image', function(e) {
                e.preventDefault();
                const targetId = $(this).data('target');
                const $input = $(document.getElementById(targetId));
                const $preview = $(document.getElementById(targetId + '-preview'));

                $input.val('').trigger('input');
                $(document.getElementById(targetId)).closest('.schemafy-image-field-wrapper').find('.schemafy-image-url-text').text((schemafy_ajax.i18n && schemafy_ajax.i18n.no_image_selected) || 'No image selected');
                if ($preview.length) $preview.hide();
                self.updateBuilderPreview();
            });
        },

        /**
         * Initialize date picker protection - prevents manual keyboard input
         */
        initDatePickerProtection: function() {
            const self = this;

            // Prevent keyboard input on date fields
            $(document).on('keydown', '.schemafy-date-picker-only', function(e) {
                // Allow Tab and Escape for navigation
                if (e.key === 'Tab' || e.key === 'Escape') {
                    return true;
                }
                // Prevent all other keyboard input
                e.preventDefault();
                return false;
            });

            // Prevent paste on date fields
            $(document).on('paste', '.schemafy-date-picker-only', function(e) {
                e.preventDefault();
                if (window.SchemafyUtilities) {
                    window.SchemafyUtilities.showNotification(t('use_date_picker', 'Please use the date picker to select a date'), 'info');
                }
                return false;
            });

            // Make clicking anywhere on the date input trigger the picker
            $(document).on('click', '.schemafy-date-picker-only', function() {
                // The native date picker should open on click
                // This ensures it works consistently
                try {
                    this.showPicker();
                } catch (e) {
                    // showPicker() not supported in some browsers, but click should work
                }
            });

            // Update preview when date changes
            $(document).on('change', '.schemafy-date-picker-only', function() {
                self.updateBuilderPreview();
            });
        },

        /**
         * Initialize numeric input protection.
         *
         * Native number inputs still let users type commas, thousands separators
         * and scientific notation ("1.000,50", "1e3"). Chrome silently clamps,
         * Firefox keeps the text visible but serializes value as "" — either way
         * the snippet ends up with broken numbers and Google validation errors.
         * These guards stop invalid characters at the keystroke/paste level.
         */
        initNumericInputProtection: function() {
            const self = this;
            const selector = '.schemafy-tab-content-v2 input[type="number"]';

            const notifyOnce = debounce(function() {
                if (window.SchemafyUtilities) {
                    window.SchemafyUtilities.showNotification(
                        t('numbers_use_dot', 'Use digits only — for decimals use a dot (e.g. 4.5), no commas or thousands separators.'),
                        'info'
                    );
                }
            }, 400);

            $(document).on('keydown', selector, function(e) {
                // Let navigation keys, shortcuts (Ctrl/Cmd+C/V/A...) and function keys through
                if (e.key.length > 1 || e.ctrlKey || e.metaKey || e.altKey) {
                    return;
                }

                const isInteger = this.getAttribute('step') === '1';
                const value = this.value;

                if (/[0-9]/.test(e.key)) {
                    return;
                }

                // One decimal dot allowed on non-integer fields
                if (e.key === '.' && !isInteger && value.indexOf('.') === -1) {
                    return;
                }

                // Leading minus for fields that can be negative (geo coordinates):
                // allowed when there is no min attribute or min is below zero
                if (e.key === '-' && value === '') {
                    const min = this.getAttribute('min');
                    if (min === null || parseFloat(min) < 0) {
                        return;
                    }
                }

                // Everything else: comma, second dot, e/E, +, letters
                e.preventDefault();
                if (e.key === ',' || e.key === '.') {
                    notifyOnce();
                }
            });

            // Sanitize pasted content instead of letting the browser reject/garble it
            $(document).on('paste', selector, function(e) {
                e.preventDefault();
                const clip = (e.originalEvent.clipboardData || window.clipboardData).getData('text');
                const isInteger = this.getAttribute('step') === '1';
                let clean = self.normalizeNumericString(clip);
                if (isInteger && clean.indexOf('.') !== -1) {
                    clean = clean.split('.')[0];
                }
                if (clean !== String(clip).trim()) {
                    notifyOnce();
                }
                this.value = clean;
                $(this).trigger('input');
            });

            // Firefox keeps invalid text visible while value reads "". Clear it on
            // blur so the user sees the field is empty instead of saving nothing.
            $(document).on('blur', selector, function() {
                if (this.validity && this.validity.badInput) {
                    this.value = '';
                    notifyOnce();
                }
            });
        },

        /**
         * Wire duration widgets: visible hours/minutes ↔ hidden ISO 8601 value.
         */
        initDurationWidgets: function() {
            // Visible inputs → hidden ISO value
            $(document).on('input change', '.schemafy-duration-hours, .schemafy-duration-minutes', function() {
                const $widget = $(this).closest('.schemafy-duration-widget');
                const hours = parseInt($widget.find('.schemafy-duration-hours').val(), 10) || 0;
                const minutes = parseInt($widget.find('.schemafy-duration-minutes').val(), 10) || 0;

                let iso = '';
                if (hours > 0 || minutes > 0) {
                    iso = 'PT' + (hours > 0 ? hours + 'H' : '') + (minutes > 0 ? minutes + 'M' : '');
                }
                $widget.find('.schemafy-duration-value').val(iso).trigger('input');
            });

            // Hidden value → visible inputs (edit mode fills via .val().trigger('change'))
            $(document).on('change', '.schemafy-duration-value', function() {
                const $widget = $(this).closest('.schemafy-duration-widget');
                const match = /^PT(?:(\d+)H)?(?:(\d+)M)?/i.exec($(this).val() || '');
                if (!match) return;
                const hours = match[1] || '';
                const minutes = match[2] || '';
                // Avoid feedback loop: only write when values differ
                const $h = $widget.find('.schemafy-duration-hours');
                const $m = $widget.find('.schemafy-duration-minutes');
                if ($h.val() !== hours) $h.val(hours);
                if ($m.val() !== minutes) $m.val(minutes);
            });
        },

        /**
         * Normalize a free-form numeric string to a JSON-safe number string.
         * Handles locale formats: "19,99" → "19.99", "1.000,50" → "1000.50",
         * "1,000.50" → "1000.50", strips currency symbols and spaces.
         */
        normalizeNumericString: function(raw) {
            if (raw === null || raw === undefined) return '';
            let v = String(raw).trim().replace(/\s/g, '');
            if (v === '') return '';

            const negative = v.charAt(0) === '-';
            v = v.replace(/[^0-9.,]/g, '');

            const lastDot = v.lastIndexOf('.');
            const lastComma = v.lastIndexOf(',');

            if (lastDot !== -1 && lastComma !== -1) {
                // Both present: the LAST separator is the decimal one
                if (lastComma > lastDot) {
                    v = v.replace(/\./g, '');          // dots were thousands
                    v = v.replace(/,/g, function(m, off, s) {
                        return off === s.lastIndexOf(',') ? '.' : '';
                    });
                } else {
                    v = v.replace(/,/g, '');           // commas were thousands
                }
            } else if (lastComma !== -1) {
                // Only commas: single comma = decimal, multiple = thousands
                v = (v.match(/,/g) || []).length === 1 ? v.replace(',', '.') : v.replace(/,/g, '');
            } else if (lastDot !== -1 && (v.match(/\./g) || []).length > 1) {
                // Multiple dots = thousands separators ("1.000.000")
                v = v.replace(/\./g, '');
            }

            return (negative ? '-' : '') + v;
        },

        /**
         * Initialize variable insertion system
         */
        initVariableSystem: function() {
            const self = this;

            // Variable groups
            const variableGroups = [
                {
                    label: 'Site & SEO',
                    icon: 'dashicons-admin-site-alt3',
                    vars: [
                        { var: '{{site_name}}', desc: 'Website name', example: 'My Website' },
                        { var: '{{site_url}}', desc: 'Website homepage URL', example: 'https://example.com' },
                        { var: '{{site_description}}', desc: 'Site tagline/description', example: 'Just another WordPress site' },
                        { var: '{{site_language}}', desc: 'Site language code', example: 'en-US' },
                        { var: '{{current_url}}', desc: 'Current page full URL', example: 'https://example.com/my-post/' },
                        { var: '{{current_year}}', desc: 'Current year', example: new Date().getFullYear().toString() },
                        { var: '{{meta_title}}', desc: 'SEO meta title (Yoast/RankMath)', example: 'My Page - My Website' },
                        { var: '{{meta_description}}', desc: 'SEO meta description', example: 'A brief page description...' },
                    ]
                },
                {
                    label: 'Post / Page',
                    icon: 'dashicons-admin-post',
                    vars: [
                        { var: '{{post_title}}', desc: 'Post/page title', example: 'My Amazing Post' },
                        { var: '{{post_excerpt}}', desc: 'Post excerpt or summary', example: 'This is a brief summary...' },
                        { var: '{{post_author}}', desc: 'Author display name', example: 'John Doe' },
                        { var: '{{post_date}}', desc: 'Publication date (ISO 8601)', example: '2026-01-14T10:00:00+00:00' },
                        { var: '{{post_modified}}', desc: 'Last modified date (ISO 8601)', example: '2026-02-18T15:30:00+00:00' },
                        { var: '{{post_url}}', desc: 'Post/page permalink', example: 'https://example.com/my-post/' },
                        { var: '{{featured_image}}', desc: 'Featured image URL', example: 'https://example.com/wp-content/uploads/image.jpg' },
                        { var: '{{category}}', desc: 'Primary category name', example: 'Technology' },
                        { var: '{{tags}}', desc: 'Tags (comma-separated)', example: 'seo, schema, wordpress' },
                        { var: '{{post_id}}', desc: 'Post/page ID', example: '142' },
                        { var: '{{post_type}}', desc: 'Post type (post, page, product)', example: 'post' },
                    ]
                }
            ];

            // Add WooCommerce group only if WooCommerce is active
            if (typeof schemafy_ajax !== 'undefined' && schemafy_ajax.has_woocommerce) {
                variableGroups.push({
                    label: 'WooCommerce',
                    icon: 'dashicons-cart',
                    vars: [
                        { var: '{{product_price}}', desc: 'Current product price', example: '29.99' },
                        { var: '{{product_regular_price}}', desc: 'Regular price (before sale)', example: '39.99' },
                        { var: '{{product_sale_price}}', desc: 'Sale price', example: '29.99' },
                        { var: '{{product_currency}}', desc: 'Store currency code', example: 'USD' },
                        { var: '{{product_sku}}', desc: 'Product SKU', example: 'WC-PROD-001' },
                        { var: '{{product_stock_status}}', desc: 'Availability (Schema.org URL)', example: 'https://schema.org/InStock' },
                        { var: '{{product_short_description}}', desc: 'Product short description', example: 'A great product for...' },
                        { var: '{{product_weight}}', desc: 'Product weight', example: '0.5' },
                        { var: '{{product_category}}', desc: 'Primary product category', example: 'Electronics' },
                        { var: '{{product_rating}}', desc: 'Average rating', example: '4.5' },
                        { var: '{{product_review_count}}', desc: 'Number of reviews', example: '23' },
                        { var: '{{product_image}}', desc: 'Main product image URL', example: 'https://example.com/wp-content/uploads/product.jpg' },
                    ]
                });
            }

            // Build grouped dropdown HTML
            const renderGroup = (group) => `
                <div style="padding: 6px 12px; margin-top: 4px; background: var(--schemafy-gray-50); border-bottom: 1px solid var(--schemafy-gray-200); border-top: 1px solid var(--schemafy-gray-200);">
                    <strong style="font-size: 12px; color: var(--schemafy-gray-700); text-transform: uppercase; letter-spacing: 0.5px;">
                        <span class="dashicons ${group.icon}" style="font-size: 14px; width: 14px; height: 14px; vertical-align: middle; margin-right: 4px;"></span>
                        ${group.label}
                    </strong>
                </div>
                <div style="padding: 4px 8px;">
                    ${group.vars.map(v => `
                        <div class="schemafy-variable-item" data-variable="${v.var}" style="padding: 8px 10px; margin: 2px 0; border-radius: 4px; cursor: pointer; transition: all 0.2s; border: 1px solid transparent;">
                            <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 2px;">
                                <code style="background: var(--schemafy-gray-100); padding: 2px 6px; border-radius: 3px; font-size: 11px; color: var(--schemafy-primary); font-weight: 600;">${v.var}</code>
                            </div>
                            <div style="font-size: 12px; color: var(--schemafy-gray-700);">${v.desc}</div>
                        </div>
                    `).join('')}
                </div>
            `;

            const dropdownHtml = `
                <div id="schemafy-variables-dropdown" style="display: none; position: absolute; background: white; border: 1px solid var(--schemafy-gray-300); border-radius: var(--schemafy-radius); box-shadow: var(--schemafy-shadow-lg); z-index: 10000; width: 380px; max-height: 450px; overflow-y: scroll; scrollbar-width: thin; scrollbar-color: #a0aec0 #f7fafc;">
                    <div style="padding: 12px; border-bottom: 1px solid var(--schemafy-gray-200); background: var(--schemafy-gray-50);">
                        <h4 style="margin: 0; font-size: 14px; font-weight: 600; color: var(--schemafy-gray-900);">
                            <span class="dashicons dashicons-shortcode" style="vertical-align: middle;"></span>
                            ${t('insert_dynamic_variable', 'Insert Dynamic Variable')}
                        </h4>
                        <p style="margin: 4px 0 0 0; font-size: 12px; color: var(--schemafy-gray-600);">${t('click_variable_to_insert', 'Click a variable to insert it at cursor position')}</p>
                    </div>
                    ${variableGroups.map(renderGroup).join('')}
                </div>
            `;

            // Append dropdown to body if not exists
            if ($('#schemafy-variables-dropdown').length === 0) {
                $('body').append(dropdownHtml);
            }

            // Use namespaced events so re-calling initVariableSystem() is safe (no duplicate handlers)
            let currentTarget = null;

            // Handle variable button click
            $(document).off('click.schemafyVarBtn').on('click.schemafyVarBtn', '.schemafy-insert-variable-btn', function(e) {
                e.preventDefault();
                e.stopPropagation();

                const $btn = $(this);
                const targetId = $btn.data('target');
                // Escape special characters in jQuery selector (handles dots, brackets, etc.)
                const escapedId = targetId ? targetId.replace(/([.[\]:])/g, '\\$1') : '';
                currentTarget = escapedId ? $('#' + escapedId) : $();

                if (!currentTarget.length) return;

                const $dropdown = $('#schemafy-variables-dropdown');
                if (!$dropdown.length) return;

                // Position dropdown near button, keeping it fully visible in viewport
                const btnOffset = $btn.offset();
                const dropdownWidth = 380;
                const dropdownMaxH = 450;
                const windowWidth = $(window).width();
                const windowHeight = $(window).height();
                const scrollTop = $(window).scrollTop();

                let left = btnOffset.left;
                let top = btnOffset.top + $btn.outerHeight() + 5;

                // Prevent overflow past right edge
                if (left + dropdownWidth > windowWidth) {
                    left = windowWidth - dropdownWidth - 20;
                }
                if (left < 0) left = 4;

                // If dropdown would go below viewport, flip it above the button
                const spaceBelow = (scrollTop + windowHeight) - top;
                if (spaceBelow < Math.min(dropdownMaxH, 200)) {
                    top = btnOffset.top - Math.min(spaceBelow > 0 ? dropdownMaxH : dropdownMaxH, dropdownMaxH) - 5;
                }

                $dropdown.css({
                    top: top + 'px',
                    left: left + 'px',
                    display: 'block'
                });
            });

            // Handle variable selection
            $(document).off('click.schemafyVarItem').on('click.schemafyVarItem', '.schemafy-variable-item', function() {
                const variable = $(this).data('variable');
                const $dropdown = $('#schemafy-variables-dropdown');

                if (currentTarget && currentTarget.length) {
                    const field = currentTarget[0];
                    const cursorPos = field.selectionStart || field.value.length;
                    const textBefore = field.value.substring(0, cursorPos);
                    const textAfter = field.value.substring(cursorPos);

                    field.value = textBefore + variable + textAfter;

                    const newPos = cursorPos + variable.length;
                    field.setSelectionRange(newPos, newPos);
                    field.focus();

                    $(field).trigger('input');

                    if (window.SchemafyUtilities) {
                        window.SchemafyUtilities.showNotification(variable + ' ' + t('variable_inserted', 'Variable inserted!'), 'success');
                    }
                }

                $dropdown.hide();
            });

            // Close dropdown when clicking outside
            $(document).off('click.schemafyVarClose').on('click.schemafyVarClose', function(e) {
                if (!$(e.target).closest('#schemafy-variables-dropdown, .schemafy-insert-variable-btn').length) {
                    $('#schemafy-variables-dropdown').hide();
                }
            });

            // Close on escape
            $(document).off('keydown.schemafyVar').on('keydown.schemafyVar', function(e) {
                if (e.key === 'Escape') {
                    $('#schemafy-variables-dropdown').hide();
                }
            });
        },

        /**
         * Initialize URL combobox — single input for search + manual entry
         */
        initUrlDropdown: function() {
            const self = this;
            const $input = $('#schemafy-url-combobox');
            const $results = $('#schemafy-url-results');

            if ($input.length === 0) return;

            let allPages = [];
            let resultsOpen = false;

            // Load all site pages once
            $.ajax({
                url: schemafy_ajax.ajax_url,
                method: 'POST',
                data: { action: 'schemafy_get_site_urls', nonce: schemafy_ajax.nonce },
                beforeSend: function() {
                    $input.attr('placeholder', t('loading_urls', 'Loading pages...'));
                },
                success: function(response) {
                    $input.attr('placeholder', 'Search pages or paste a URL...');
                    if (response.success && response.data.urls) {
                        allPages = response.data.urls;
                    }
                },
                error: function() {
                    $input.attr('placeholder', 'Paste a URL and press Enter...');
                }
            });

            function esc(str) {
                return window.SchemafyUtilities.escapeHtml(str);
            }

            function renderResults(q) {
                const isUrl = /^https?:\/\//i.test(q.trim());
                const lower = q.toLowerCase();
                let html = '';

                if (isUrl) {
                    html += `<div class="schemafy-url-result schemafy-url-result-paste" data-url="${esc(q.trim())}" data-title="${esc(q.trim())}">
                        <span class="dashicons dashicons-admin-links"></span>
                        <div class="schemafy-url-result-body">
                            <span class="schemafy-url-result-title">Add this URL</span>
                            <span class="schemafy-url-result-url">${esc(q.trim())}</span>
                        </div>
                    </div>`;
                } else {
                    const filtered = q.length >= 1
                        ? allPages.filter(p => (p.title + ' ' + p.url).toLowerCase().includes(lower)).slice(0, 10)
                        : allPages.slice(0, 10);

                    if (filtered.length === 0 && q.length >= 2) {
                        html = `<div class="schemafy-url-result-empty">No pages found — press Enter to add as URL</div>`;
                    } else {
                        const grouped = {};
                        filtered.forEach(p => {
                            const label = p.post_type_label || 'Other';
                            if (!grouped[label]) grouped[label] = [];
                            grouped[label].push(p);
                        });
                        Object.keys(grouped).sort().forEach(type => {
                            html += `<div class="schemafy-url-result-group">${esc(type)}</div>`;
                            grouped[type].forEach(p => {
                                html += `<div class="schemafy-url-result" data-url="${esc(p.url)}" data-title="${esc(p.title)}">
                                    <span class="dashicons dashicons-admin-page"></span>
                                    <div class="schemafy-url-result-body">
                                        <span class="schemafy-url-result-title">${esc(p.title)}</span>
                                        <span class="schemafy-url-result-url">${esc(p.url)}</span>
                                    </div>
                                </div>`;
                            });
                        });
                    }
                }

                $results.html(html);
                if (html) {
                    $results.show();
                    resultsOpen = true;
                } else {
                    $results.hide();
                    resultsOpen = false;
                }
            }

            $input.on('focus', function() {
                renderResults($(this).val());
            });

            $input.on('input', function() {
                renderResults($(this).val());
            });

            $input.on('keydown', function(e) {
                if (e.key === 'Enter') {
                    e.preventDefault();
                    const val = $(this).val().trim();
                    if (!val) return;
                    // Pick first visible result or treat as raw URL
                    const $first = $results.find('.schemafy-url-result').first();
                    if ($first.length) {
                        self.addUrlChip($first.data('url'), $first.data('title'));
                    } else {
                        self.addUrlChip(val, val);
                    }
                    $(this).val('');
                    $results.hide();
                    resultsOpen = false;
                } else if (e.key === 'Escape') {
                    $results.hide();
                    resultsOpen = false;
                }
            });

            // Click a result
            $results.on('click', '.schemafy-url-result', function() {
                self.addUrlChip($(this).data('url'), $(this).data('title'));
                $input.val('').focus();
                $results.hide();
                resultsOpen = false;
            });

            // Hide on outside click
            $(document).on('click.schemafyCombobox', function(e) {
                if (!$(e.target).closest('.schemafy-url-dropdown-container').length) {
                    $results.hide();
                    resultsOpen = false;
                }
            });

            // Remove chip handler (delegated)
            $('#schemafy-selected-urls').on('click', '.schemafy-chip-remove', function() {
                const url = $(this).closest('.schemafy-url-chip').data('url');
                self.removeUrlChip(url);
            });
        },

        /**
         * Add a URL chip to the selected URLs list
         */
        addUrlChip: function(url, displayText) {
            const $container = $('#schemafy-selected-urls');
            const $hidden = $('#schemafy-target-urls');

            // Get current URLs
            let urls = [];
            try { urls = JSON.parse($hidden.val() || '[]'); } catch(e) { urls = []; }

            // Block adding more URLs in edit mode (one URL per schema)
            const editingId = $('#schemafy-dynamic-schema-form').data('editing-id');
            if (editingId && urls.length >= 1) {
                if (window.SchemafyUtilities) {
                    window.SchemafyUtilities.showNotification(t('only_one_url', 'Only one URL allowed when editing a schema'), 'warning');
                }
                return;
            }

            // Check for duplicates
            if (urls.indexOf(url) !== -1) {
                if (window.SchemafyUtilities) {
                    window.SchemafyUtilities.showNotification(t('url_already_added', 'This URL is already added'), 'warning');
                }
                return;
            }

            urls.push(url);
            $hidden.val(JSON.stringify(urls));

            const label = displayText || url;
            const chipHtml = `
                <div class="schemafy-url-chip" data-url="${window.SchemafyUtilities.escapeHtml(url)}">
                    <span class="dashicons dashicons-admin-links"></span>
                    <span class="schemafy-url-chip-text" title="${window.SchemafyUtilities.escapeHtml(url)}">${window.SchemafyUtilities.escapeHtml(label)}</span>
                    <button type="button" class="schemafy-chip-remove" title="Remove">
                        <span class="dashicons dashicons-no-alt"></span>
                    </button>
                </div>
            `;
            $container.append(chipHtml);
        },

        /**
         * Remove a URL chip from the selected URLs list
         */
        removeUrlChip: function(url) {
            // Block removing URLs in edit mode
            const editingId = $('#schemafy-dynamic-schema-form').data('editing-id');
            if (editingId) {
                if (window.SchemafyUtilities) {
                    window.SchemafyUtilities.showNotification(t('cannot_remove_url', 'Cannot remove URL while editing a schema'), 'warning');
                }
                return;
            }

            const $container = $('#schemafy-selected-urls');
            const $hidden = $('#schemafy-target-urls');

            let urls = [];
            try { urls = JSON.parse($hidden.val() || '[]'); } catch(e) { urls = []; }

            urls = urls.filter(u => u !== url);
            $hidden.val(JSON.stringify(urls));

            $container.find(`.schemafy-url-chip[data-url="${CSS.escape(url)}"]`).remove();
        },

        /**
         * Get all selected URLs from the chip list
         */
        getSelectedUrls: function() {
            const $hidden = $('#schemafy-target-urls');
            try {
                return JSON.parse($hidden.val() || '[]');
            } catch(e) {
                return [];
            }
        },

        /**
         * Update JSON preview
         */
        updateBuilderPreview: function() {
            const $form = $('#schemafy-dynamic-schema-form');
            if ($form.length === 0) return;

            const type = $form.find('[name="schema_type"]').val();
            if (!type) return;

            const $preview = $('#schemafy-json-preview code');
            if ($preview.length === 0) return;

            const formData = this.getFormData($form);
            const buildHint = $form.data('schema-builder-type');
            let schema;
            try {
                schema = this.generateSchemaFromForm(type, formData, buildHint);
                const editingId = $form.data('editing-id');
                const originalSchema = $form.data('original-schema-data');
                if (editingId && originalSchema && typeof originalSchema === 'object') {
                    schema = this.mergeEditedSchema(originalSchema, schema);
                }
            } catch (e) {
                console.error('[SCHEMAFY] Preview generation error:', e);
                $preview.text('{\n  "@context": "https://schema.org",\n  "@type": "' + type + '",\n  "name": "..."\n}');
                return;
            }

            // If schema has no actual data yet (only @context and @type), show a typed placeholder
            const dataKeys = Object.keys(schema).filter(k => k !== '@context' && k !== '@type');
            if (dataKeys.length === 0) {
                const schemaType = schema['@type'] || type;
                $preview.text(
                    '{\n  "@context": "https://schema.org",\n  "@type": "' + schemaType + '",\n  "name": "..."\n}'
                );
                return;
            }

            try {
                $preview.text(JSON.stringify(schema, null, 2));
            } catch (e) {
                console.error('[SCHEMAFY] Preview stringify error:', e);
                $preview.text('{\n  "@context": "https://schema.org",\n  "@type": "' + type + '",\n  "name": "..."\n}');
            }

            this.updateFriendlyPreview();
        },

        /**
         * Get form data as object.
         *
         * Values from number inputs are normalized (commas, thousands
         * separators, currency symbols) as a last line of defense — autofill
         * and programmatic value changes bypass the keystroke guards.
         */
        getFormData: function($form) {
            const self = this;
            const data = {};
            $form.serializeArray().forEach(item => {
                let value = item.value;
                const el = $form.find('[name="' + item.name + '"]').get(0);
                if (el && el.type === 'number' && value !== '') {
                    value = self.normalizeNumericString(value);
                }
                data[item.name] = value;
            });
            return data;
        },

        /**
         * Normalize a date-only string (YYYY-MM-DD) to ISO 8601 with time component.
         * Date inputs return bare dates; Google's Rich Results requires at minimum YYYY-MM-DDT00:00:00.
         */
        toIsoDate: function(value) {
            if (!value) return value;
            // Already has a time component — return as-is
            if (value.indexOf('T') !== -1) return value;
            // Bare date — append midnight UTC offset
            return value + 'T00:00:00+00:00';
        },

        /**
         * Generate Schema.org JSON from form data
         */
        generateSchemaFromForm: function(type, data, buildHint) {
            // Map builder card type to correct Schema.org type
            const schemaType = this.cardTypeToSchemaType[type] || type;
            const buildType = this.resolveSchemaBuildType(schemaType, type, buildHint);

            const schema = {
                "@context": "https://schema.org",
                "@type": schemaType
            };

            const arrayFields = {};
            const regularFields = {};

            // Fields that should be skipped (meta fields, not schema properties)
            const skipFields = ['schema_type', '_target_url', '_target_url_manual', '_target_urls', 'schema_name'];

            Object.entries(data).forEach(([key, value]) => {
                if (!value || skipFields.includes(key)) {
                    return;
                }

                const arrayMatch = key.match(/^(\w+)\[(\d+)\]\[(\w+)\]$/);
                if (arrayMatch) {
                    const [, fieldName, index, subFieldName] = arrayMatch;
                    if (!arrayFields[fieldName]) arrayFields[fieldName] = [];
                    if (!arrayFields[fieldName][index]) arrayFields[fieldName][index] = {};
                    arrayFields[fieldName][index][subFieldName] = value;
                } else {
                    regularFields[key] = value;
                }
            });

            // Handle type-specific schema structures
            if (buildType === 'Product') {
                this.buildProductSchema(schema, regularFields);
            } else if (buildType === 'Article') {
                this.buildArticleSchema(schema, regularFields);
            } else if (buildType === 'LocalBusiness') {
                this.buildLocalBusinessSchema(schema, regularFields);
            } else if (buildType === 'Organization') {
                this.buildOrganizationSchema(schema, regularFields);
            } else if (buildType === 'Event') {
                this.buildEventSchema(schema, regularFields);
            } else if (buildType === 'Recipe') {
                this.buildRecipeSchema(schema, regularFields);
            } else if (buildType === 'HowTo') {
                this.buildHowToSchema(schema, regularFields);
            } else if (buildType === 'VideoObject') {
                this.buildVideoSchema(schema, regularFields);
            } else if (buildType === 'Review') {
                this.buildReviewSchema(schema, regularFields);
            } else if (buildType === 'Course') {
                this.buildCourseSchema(schema, regularFields);
            } else if (buildType === 'Service') {
                this.buildServiceSchema(schema, regularFields);
            } else if (buildType === 'Person') {
                this.buildPersonSchema(schema, regularFields);
            } else if (buildType === 'JobPosting') {
                this.buildJobPostingSchema(schema, regularFields);
            } else if (buildType === 'WebPage') {
                this.buildWebPageSchema(schema, regularFields);
            } else {
                // Default: add all fields directly
                Object.entries(regularFields).forEach(([key, value]) => {
                    schema[key] = value;
                });
            }

            // Handle array fields (FAQ, Breadcrumbs, Recipe, HowTo, etc.)
            // Note: items array may have undefined entries when items are removed from middle
            Object.entries(arrayFields).forEach(([fieldName, items]) => {
                if (fieldName === 'mainEntity') {
                    // FAQPage questions
                    schema.mainEntity = items.filter(item => item && (item.questionName || item.acceptedAnswer)).map(item => ({
                        "@type": "Question",
                        "name": item.questionName || "",
                        "acceptedAnswer": {
                            "@type": "Answer",
                            "text": item.acceptedAnswer || ""
                        }
                    }));
                } else if (fieldName === 'sameAs') {
                    // Social media links - flatten to array of URLs
                    schema.sameAs = items.filter(item => item && item.url).map(item => item.url);
                } else if (fieldName === 'itemListElement') {
                    // BreadcrumbList items - add position automatically
                    schema.itemListElement = items.filter(item => item && (item.itemName || item.itemUrl)).map((item, index) => ({
                        "@type": "ListItem",
                        "position": index + 1,
                        "name": item.itemName || "",
                        "item": item.itemUrl || ""
                    }));
                } else if (fieldName === 'recipeIngredient') {
                    // Recipe ingredients - flatten to array of strings
                    schema.recipeIngredient = items.filter(item => item && item.ingredient).map(item => item.ingredient);
                } else if (fieldName === 'recipeInstructions') {
                    // Recipe instructions - HowToStep objects
                    schema.recipeInstructions = items.filter(item => item && item.text).map((item, index) => {
                        const step = {
                            "@type": "HowToStep",
                            "text": item.text
                        };
                        if (item.name) step.name = item.name;
                        if (item.image) step.image = item.image;
                        return step;
                    });
                } else if (fieldName === 'step') {
                    // HowTo steps - HowToStep objects
                    schema.step = items.filter(item => item && (item.text || item.name)).map((item) => {
                        const step = { "@type": "HowToStep" };
                        if (item.name) step.name = item.name;
                        if (item.text) step.text = item.text;
                        if (item.image) step.image = item.image;
                        if (item.url) step.url = item.url;
                        if (item.duration) step.duration = item.duration;
                        return step;
                    });
                } else if (fieldName === 'supply') {
                    // HowTo supplies - HowToSupply objects
                    schema.supply = items.filter(item => item && item.name).map(item => {
                        const supply = {
                            "@type": "HowToSupply",
                            "name": item.name
                        };
                        if (item.image) supply.image = item.image;
                        if (item.estimatedCost) {
                            supply.estimatedCost = {
                                "@type": "MonetaryAmount",
                                "value": item.estimatedCost,
                                "currency": "USD"
                            };
                        }
                        return supply;
                    });
                } else if (fieldName === 'tool') {
                    // HowTo tools - HowToTool objects
                    schema.tool = items.filter(item => item && item.name).map(item => {
                        const tool = {
                            "@type": "HowToTool",
                            "name": item.name
                        };
                        if (item.image) tool.image = item.image;
                        return tool;
                    });
                } else if (fieldName === 'hasPart') {
                    // VideoObject chapters - Clip objects
                    schema.hasPart = items.filter(item => item && item.name).map(item => {
                        const clip = {
                            "@type": "Clip",
                            "name": item.name
                        };
                        if (item.startOffset !== undefined) clip.startOffset = parseInt(item.startOffset);
                        if (item.endOffset !== undefined) clip.endOffset = parseInt(item.endOffset);
                        if (item.url) clip.url = item.url;
                        return clip;
                    });
                } else if (fieldName === 'positiveNotes') {
                    // Review pros - ItemList
                    const validItems = items.filter(item => item && item.name);
                    if (validItems.length > 0) {
                        schema.positiveNotes = {
                            "@type": "ItemList",
                            "itemListElement": validItems.map((item, index) => ({
                                "@type": "ListItem",
                                "position": index + 1,
                                "name": item.name
                            }))
                        };
                    }
                } else if (fieldName === 'negativeNotes') {
                    // Review cons - ItemList
                    const validItems = items.filter(item => item && item.name);
                    if (validItems.length > 0) {
                        schema.negativeNotes = {
                            "@type": "ItemList",
                            "itemListElement": validItems.map((item, index) => ({
                                "@type": "ListItem",
                                "position": index + 1,
                                "name": item.name
                            }))
                        };
                    }
                } else if (fieldName === 'review') {
                    // Product reviews - Review objects
                    schema.review = items.filter(item => item && (item.author || item.reviewBody)).map(item => {
                        const review = { "@type": "Review" };
                        if (item.name) review.name = item.name;
                        if (item.author) {
                            review.author = { "@type": "Person", "name": item.author };
                        }
                        if (item.reviewBody) review.reviewBody = item.reviewBody;
                        if (item.reviewRating) {
                            review.reviewRating = {
                                "@type": "Rating",
                                "ratingValue": parseFloat(item.reviewRating),
                                "bestRating": 5,
                                "worstRating": 1
                            };
                        }
                        if (item.datePublished) review.datePublished = this.toIsoDate(item.datePublished);
                        return review;
                    });
                } else if (fieldName === 'openingHoursSpecification') {
                    // LocalBusiness opening hours
                    schema.openingHoursSpecification = items.filter(item => item && item.dayOfWeek).map(item => {
                        const hours = {
                            "@type": "OpeningHoursSpecification",
                            "dayOfWeek": item.dayOfWeek
                        };
                        if (item.opens) hours.opens = item.opens;
                        if (item.closes) hours.closes = item.closes;
                        return hours;
                    });
                } else if (fieldName === 'hasCourseInstance') {
                    // Course instances
                    schema.hasCourseInstance = items.filter(item => item && (item.courseMode || item.startDate)).map(item => {
                        const instance = { "@type": "CourseInstance" };
                        if (item.courseMode) instance.courseMode = item.courseMode;
                        if (item.startDate) instance.startDate = this.toIsoDate(item.startDate);
                        if (item.endDate) instance.endDate = this.toIsoDate(item.endDate);
                        if (item.instructor) {
                            instance.instructor = { "@type": "Person", "name": item.instructor };
                        }
                        return instance;
                    });
                } else if (fieldName === 'breadcrumb') {
                    // WebPage breadcrumb — nested BreadcrumbList with itemListElement
                    const validItems = items.filter(item => item && (item.name || item.item));
                    if (validItems.length > 0) {
                        schema.breadcrumb = {
                            "@type": "BreadcrumbList",
                            "itemListElement": validItems.map((item, index) => ({
                                "@type": "ListItem",
                                "position": item.position ? parseInt(item.position) : (index + 1),
                                "name": item.name || "",
                                "item": item.item || ""
                            }))
                        };
                    }
                } else if (fieldName === 'significantLink') {
                    // WebPage significant links — flatten to URL string array
                    schema.significantLink = items.filter(item => item && item.url).map(item => item.url);
                } else {
                    // Filter out undefined AND empty objects (all-empty sub-fields)
                    const validItems = items.filter(item => {
                        if (!item) return false;
                        if (typeof item === 'object') {
                            return Object.values(item).some(v => v !== '' && v !== null && v !== undefined);
                        }
                        return Boolean(item);
                    });
                    if (validItems.length > 0) {
                        schema[fieldName] = validItems;
                    }
                }
            });

            return schema;
        },

        /**
         * Build Product schema with proper nested structures
         */
        buildProductSchema: function(schema, fields) {
            // Direct Product properties
            const directProps = ['name', 'description', 'image', 'sku', 'gtin', 'mpn'];
            directProps.forEach(prop => {
                if (fields[prop]) schema[prop] = fields[prop];
            });

            // Brand - nested object
            if (fields.brand) {
                schema.brand = {
                    "@type": "Brand",
                    "name": fields.brand
                };
            }

            // Offers - support dot-notation
            const offersPrice = fields['offers.price'] || fields.price;
            const offersCurrency = fields['offers.priceCurrency'] || fields.priceCurrency || fields.currency;
            const offersAvailability = fields['offers.availability'] || fields.availability;
            if (offersPrice || offersCurrency || offersAvailability) {
                schema.offers = {
                    "@type": "Offer"
                };
                if (offersPrice) schema.offers.price = offersPrice;
                if (offersCurrency) schema.offers.priceCurrency = offersCurrency;
                if (offersAvailability) {
                    if (offersAvailability.startsWith('https://schema.org/')) {
                        schema.offers.availability = offersAvailability;
                    } else {
                        schema.offers.availability = "https://schema.org/" + offersAvailability;
                    }
                }
                const offersUrl = fields['offers.url'] || fields.url;
                if (offersUrl) schema.offers.url = offersUrl;
            }

            // AggregateRating — both ratingValue AND reviewCount required by Google
            const ratingValue = fields['aggregateRating.ratingValue'] || fields.ratingValue;
            const reviewCount = fields['aggregateRating.reviewCount'] || fields.reviewCount;
            if (ratingValue && reviewCount) {
                const bestRating  = fields['aggregateRating.bestRating']  || fields.bestRating;
                const worstRating = fields['aggregateRating.worstRating'] || fields.worstRating;
                schema.aggregateRating = {
                    "@type": "AggregateRating",
                    "ratingValue":  parseFloat(ratingValue),
                    "reviewCount":  parseInt(reviewCount),
                    "bestRating":   bestRating  ? parseFloat(bestRating)  : 5,
                    "worstRating":  worstRating ? parseFloat(worstRating) : 1
                };
            }

            // ShippingDetails (optional — Google Merchant Listings)
            const shippingRate     = fields['shippingDetails.shippingRate'];
            const shippingCurrency = fields['shippingDetails.shippingCurrency'];
            const shippingDest     = fields['shippingDetails.shippingDestination'];
            if (shippingDest || shippingCurrency || shippingRate !== undefined) {
                schema.shippingDetails = { "@type": "OfferShippingDetails" };
                if (shippingRate !== undefined && shippingCurrency) {
                    schema.shippingDetails.shippingRate = {
                        "@type": "MonetaryAmount",
                        "value": parseFloat(shippingRate) || 0,
                        "currency": shippingCurrency
                    };
                }
                if (shippingDest) {
                    schema.shippingDetails.shippingDestination = {
                        "@type": "DefinedRegion",
                        "addressCountry": shippingDest
                    };
                }
            }

            // Merchant Return Policy (optional — Google Merchant Listings)
            const returnCategory   = fields['hasMerchantReturnPolicy.returnPolicyCategory'];
            const returnDays       = fields['hasMerchantReturnPolicy.merchantReturnDays'];
            const returnShipping   = fields['hasMerchantReturnPolicy.returnShippingFeesAmount'];
            if (returnCategory) {
                schema.hasMerchantReturnPolicy = {
                    "@type":                 "MerchantReturnPolicy",
                    "returnPolicyCategory":  returnCategory
                };
                if (returnDays)    schema.hasMerchantReturnPolicy.merchantReturnDays = parseInt(returnDays);
                if (returnShipping !== undefined) {
                    schema.hasMerchantReturnPolicy.returnShippingFeesAmount = {
                        "@type":    "MonetaryAmount",
                        "value":    parseFloat(returnShipping) || 0,
                        "currency": offersCurrency || 'USD'
                    };
                }
            }
        },

        /**
         * Build Article schema with proper nested structures
         */
        buildArticleSchema: function(schema, fields) {
            // Direct Article properties
            const directProps = ['headline', 'description', 'image', 'inLanguage',
                                 'articleSection', 'keywords', 'mainEntityOfPage', 'wordCount', 'articleBody'];
            directProps.forEach(prop => {
                if (fields[prop]) schema[prop] = fields[prop];
            });
            // Date fields — normalize to ISO 8601 with timezone
            if (fields.datePublished) schema.datePublished = this.toIsoDate(fields.datePublished);
            if (fields.dateModified)  schema.dateModified  = this.toIsoDate(fields.dateModified);

            // Author - support dot-notation (JSON config) and legacy flat names
            const authorName = fields['author.name'] || fields.authorName || fields.author;
            if (authorName) {
                schema.author = {
                    "@type": fields['author.type'] || fields.authorType || "Person",
                    "name": authorName
                };
                const authorUrl = fields['author.url'] || fields.authorUrl;
                const authorImage = fields['author.image'] || fields.authorImage;
                if (authorUrl) schema.author.url = authorUrl;
                if (authorImage) schema.author.image = authorImage;
            }

            // Publisher
            const publisherName = fields['publisher.name'] || fields.publisherName;
            const publisherLogo = fields['publisher.logo'] || fields.publisherLogo;
            if (publisherName || publisherLogo) {
                schema.publisher = {
                    "@type": "Organization"
                };
                if (publisherName) schema.publisher.name = publisherName;
                if (publisherLogo) {
                    schema.publisher.logo = {
                        "@type": "ImageObject",
                        "url": publisherLogo
                    };
                }
                const publisherUrl = fields['publisher.url'] || fields.publisherUrl;
                if (publisherUrl) schema.publisher.url = publisherUrl;
            }
        },

        /**
         * Build LocalBusiness schema with proper nested structures
         */
        buildLocalBusinessSchema: function(schema, fields) {
            // Direct properties
            const directProps = ['name', 'telephone', 'url', 'image', 'priceRange', 'description',
                                 'email', 'paymentAccepted', 'currenciesAccepted', 'servesCuisine', 'hasMap'];
            directProps.forEach(prop => {
                if (fields[prop]) schema[prop] = fields[prop];
            });

            // Address - support dot-notation (JSON config) and legacy flat names
            const streetAddress = fields['address.streetAddress'] || fields.address;
            const addressLocality = fields['address.addressLocality'] || fields.city;
            const addressRegion = fields['address.addressRegion'] || fields.state;
            const postalCode = fields['address.postalCode'] || fields.postalCode;
            const addressCountry = fields['address.addressCountry'] || fields.country;
            if (streetAddress || addressLocality || addressRegion || postalCode || addressCountry) {
                schema.address = {
                    "@type": "PostalAddress"
                };
                if (streetAddress) schema.address.streetAddress = streetAddress;
                if (addressLocality) schema.address.addressLocality = addressLocality;
                if (addressRegion) schema.address.addressRegion = addressRegion;
                if (postalCode) schema.address.postalCode = postalCode;
                if (addressCountry) schema.address.addressCountry = addressCountry;
            }

            // Opening hours
            if (fields.openingHours) {
                schema.openingHours = fields.openingHours;
            }

            // Geo coordinates - support dot-notation
            const latitude = fields['geo.latitude'] || fields.latitude;
            const longitude = fields['geo.longitude'] || fields.longitude;
            if (latitude && longitude) {
                schema.geo = {
                    "@type": "GeoCoordinates",
                    "latitude": latitude,
                    "longitude": longitude
                };
            }

            // Aggregate Rating - support dot-notation
            const ratingValue = fields['aggregateRating.ratingValue'] || fields.ratingValue;
            const reviewCount = fields['aggregateRating.reviewCount'] || fields.reviewCount;
            if (ratingValue && reviewCount) {
                const bestRating  = fields['aggregateRating.bestRating']  || fields.bestRating;
                const worstRating = fields['aggregateRating.worstRating'] || fields.worstRating;
                schema.aggregateRating = {
                    "@type":       "AggregateRating",
                    "ratingValue": parseFloat(ratingValue),
                    "reviewCount": parseInt(reviewCount),
                    "bestRating":  bestRating  ? parseFloat(bestRating)  : 5,
                    "worstRating": worstRating ? parseFloat(worstRating) : 1
                };
            }
        },

        /**
         * Build Organization schema with proper structures
         */
        buildOrganizationSchema: function(schema, fields) {
            // Direct Organization properties
            const directProps = ['name', 'url', 'logo', 'description', 'telephone', 'email', 'foundingDate'];
            directProps.forEach(prop => {
                if (fields[prop]) schema[prop] = fields[prop];
            });

            // Address - support dot-notation
            const streetAddress = fields['address.streetAddress'] || fields.streetAddress;
            const addressLocality = fields['address.addressLocality'] || fields.addressLocality;
            const addressRegion = fields['address.addressRegion'] || fields.addressRegion;
            const postalCode = fields['address.postalCode'] || fields.postalCode;
            const addressCountry = fields['address.addressCountry'] || fields.addressCountry;
            if (streetAddress || addressLocality || addressRegion || postalCode || addressCountry) {
                schema.address = {
                    "@type": "PostalAddress"
                };
                if (streetAddress) schema.address.streetAddress = streetAddress;
                if (addressLocality) schema.address.addressLocality = addressLocality;
                if (addressRegion) schema.address.addressRegion = addressRegion;
                if (postalCode) schema.address.postalCode = postalCode;
                if (addressCountry) schema.address.addressCountry = addressCountry;
            }

            // ContactPoint - support dot-notation
            const cpTelephone = fields['contactPoint.telephone'] || fields.telephone;
            const cpContactType = fields['contactPoint.contactType'] || fields.contactType;
            const cpEmail = fields['contactPoint.email'];
            const cpAreaServed = fields['contactPoint.areaServed'] || fields.areaServed;
            const cpLanguage = fields['contactPoint.availableLanguage'] || fields.availableLanguage;
            if (cpContactType || cpTelephone || cpEmail) {
                schema.contactPoint = {
                    "@type": "ContactPoint"
                };
                if (cpTelephone) schema.contactPoint.telephone = cpTelephone;
                if (cpContactType) schema.contactPoint.contactType = cpContactType;
                if (cpEmail) schema.contactPoint.email = cpEmail;
                if (cpAreaServed) schema.contactPoint.areaServed = cpAreaServed;
                if (cpLanguage) schema.contactPoint.availableLanguage = cpLanguage;
            }
        },

        /**
         * Build Event schema with proper structures
         */
        buildEventSchema: function(schema, fields) {
            const directProps = ['name', 'description', 'image', 'eventStatus', 'eventAttendanceMode'];
            directProps.forEach(prop => {
                if (fields[prop]) schema[prop] = fields[prop];
            });

            // Google requires ISO 8601 with time component for Event dates
            if (fields.startDate) schema.startDate = this.toIsoDate(fields.startDate);
            if (fields.endDate)   schema.endDate   = this.toIsoDate(fields.endDate);

            // Location - support dot-notation
            const locationName = fields['location.name'] || fields.locationName;
            const locStreetAddress = fields['location.streetAddress'] || fields.streetAddress;
            const locAddressLocality = fields['location.addressLocality'] || fields.addressLocality;
            if (locationName || locStreetAddress) {
                schema.location = {
                    "@type": "Place",
                    "name": locationName || ""
                };
                const locAddressRegion = fields['location.addressRegion'] || fields.addressRegion;
                const locPostalCode = fields['location.postalCode'] || fields.postalCode;
                const locAddressCountry = fields['location.addressCountry'] || fields.addressCountry;
                if (locStreetAddress || locAddressLocality) {
                    schema.location.address = {
                        "@type": "PostalAddress"
                    };
                    if (locStreetAddress) schema.location.address.streetAddress = locStreetAddress;
                    if (locAddressLocality) schema.location.address.addressLocality = locAddressLocality;
                    if (locAddressRegion) schema.location.address.addressRegion = locAddressRegion;
                    if (locPostalCode) schema.location.address.postalCode = locPostalCode;
                    if (locAddressCountry) schema.location.address.addressCountry = locAddressCountry;
                }
            }

            // Organizer - support dot-notation
            const organizerName = fields['organizer.name'] || fields.organizerName;
            if (organizerName) {
                schema.organizer = {
                    "@type": fields['organizer.type'] || fields.organizerType || "Organization",
                    "name": organizerName
                };
                const organizerUrl = fields['organizer.url'] || fields.organizerUrl;
                if (organizerUrl) schema.organizer.url = organizerUrl;
            }

            // Performer - support dot-notation
            const performerName = fields['performer.name'] || fields.performerName;
            if (performerName) {
                schema.performer = {
                    "@type": fields['performer.type'] || fields.performerType || "Person",
                    "name": performerName
                };
            }

            // Offers (tickets) - support dot-notation
            const offersPrice = fields['offers.price'] || fields.price;
            const offersAvailability = fields['offers.availability'] || fields.availability;
            if (offersPrice !== undefined || offersAvailability) {
                schema.offers = {
                    "@type": "Offer"
                };
                if (offersPrice !== undefined) schema.offers.price = offersPrice;
                const offersCurrency = fields['offers.priceCurrency'] || fields.priceCurrency;
                if (offersCurrency) schema.offers.priceCurrency = offersCurrency;
                if (offersAvailability) schema.offers.availability = offersAvailability;
                const offersUrl = fields['offers.url'];
                if (offersUrl) schema.offers.url = offersUrl;
                const offersValidFrom = fields['offers.validFrom'] || fields.validFrom;
                if (offersValidFrom) schema.offers.validFrom = offersValidFrom;
            }
        },

        /**
         * Build Recipe schema with proper structures
         */
        buildRecipeSchema: function(schema, fields) {
            const directProps = ['name', 'description', 'image', 'datePublished', 'prepTime', 'cookTime', 'totalTime',
                                 'recipeYield', 'recipeCategory', 'recipeCuisine', 'keywords', 'suitableForDiet'];
            directProps.forEach(prop => {
                if (fields[prop]) schema[prop] = fields[prop];
            });

            // Author - support dot-notation
            const authorName = fields['author.name'] || fields.authorName;
            if (authorName) {
                schema.author = {
                    "@type": fields['author.type'] || fields.authorType || "Person",
                    "name": authorName
                };
            }

            // Nutrition - support dot-notation
            const calories = fields['nutrition.calories'] || fields.calories;
            const carbs = fields['nutrition.carbohydrateContent'] || fields.carbohydrateContent;
            const protein = fields['nutrition.proteinContent'] || fields.proteinContent;
            const fat = fields['nutrition.fatContent'] || fields.fatContent;
            if (calories || carbs || protein || fat) {
                schema.nutrition = {
                    "@type": "NutritionInformation"
                };
                if (calories) schema.nutrition.calories = calories;
                if (carbs) schema.nutrition.carbohydrateContent = carbs;
                if (protein) schema.nutrition.proteinContent = protein;
                if (fat) schema.nutrition.fatContent = fat;
                const fiber = fields['nutrition.fiberContent'] || fields.fiberContent;
                const sugar = fields['nutrition.sugarContent'] || fields.sugarContent;
                const sodium = fields['nutrition.sodiumContent'] || fields.sodiumContent;
                if (fiber) schema.nutrition.fiberContent = fiber;
                if (sugar) schema.nutrition.sugarContent = sugar;
                if (sodium) schema.nutrition.sodiumContent = sodium;
            }

            // Aggregate Rating - support dot-notation
            const ratingValue = fields['aggregateRating.ratingValue'] || fields.ratingValue;
            const reviewCount = fields['aggregateRating.reviewCount'] || fields.reviewCount;
            if (ratingValue && reviewCount) {
                const bestRating  = fields['aggregateRating.bestRating']  || fields.bestRating;
                const worstRating = fields['aggregateRating.worstRating'] || fields.worstRating;
                schema.aggregateRating = {
                    "@type":       "AggregateRating",
                    "ratingValue": parseFloat(ratingValue),
                    "reviewCount": parseInt(reviewCount),
                    "bestRating":  bestRating  ? parseFloat(bestRating)  : 5,
                    "worstRating": worstRating ? parseFloat(worstRating) : 1
                };
            }

            // Video — emit only when fields are present; the validator enforces
            // VideoObject completeness so a URL-only video can't slip through and
            // fail Google with "missing uploadDate/name/thumbnailUrl".
            const vName  = fields['video.name'];
            const vDesc  = fields['video.description'];
            const vThumb = fields['video.thumbnailUrl'];
            const vDate  = fields['video.uploadDate'];
            const vUrl   = fields['video.contentUrl'] || fields.video;
            if (vName || vDesc || vThumb || vDate || vUrl) {
                schema.video = { "@type": "VideoObject" };
                if (vName)  schema.video.name = vName;
                if (vDesc)  schema.video.description = vDesc;
                if (vThumb) schema.video.thumbnailUrl = vThumb;
                if (vDate)  schema.video.uploadDate = this.toIsoDate(vDate);
                if (vUrl)   schema.video.contentUrl = vUrl;
            }
        },

        /**
         * Build HowTo schema with proper structures
         */
        buildHowToSchema: function(schema, fields) {
            const directProps = ['name', 'description', 'image', 'totalTime', 'prepTime', 'performTime'];
            directProps.forEach(prop => {
                if (fields[prop]) schema[prop] = fields[prop];
            });

            // Estimated Cost - support dot-notation
            const costValue = fields['estimatedCost.value'] || fields.value;
            const costCurrency = fields['estimatedCost.currency'] || fields.currency;
            if (costValue || costCurrency) {
                schema.estimatedCost = {
                    "@type": "MonetaryAmount"
                };
                if (costValue) schema.estimatedCost.value = costValue;
                if (costCurrency) schema.estimatedCost.currency = costCurrency;
            }

            // Author - support dot-notation
            if (fields['author.name']) {
                schema.author = {
                    "@type": "Person",
                    "name": fields['author.name']
                };
                if (fields['author.url']) schema.author.url = fields['author.url'];
            }

            // Video - support dot-notation
            if (fields['video.name'] || fields['video.contentUrl']) {
                schema.video = {
                    "@type": "VideoObject"
                };
                if (fields['video.name']) schema.video.name = fields['video.name'];
                if (fields['video.description']) schema.video.description = fields['video.description'];
                if (fields['video.thumbnailUrl']) schema.video.thumbnailUrl = fields['video.thumbnailUrl'];
                if (fields['video.contentUrl']) schema.video.contentUrl = fields['video.contentUrl'];
                if (fields['video.uploadDate']) schema.video.uploadDate = fields['video.uploadDate'];
            }
        },

        /**
         * Build VideoObject schema with proper structures
         */
        buildVideoSchema: function(schema, fields) {
            const directProps = ['name', 'description', 'thumbnailUrl', 'duration', 'contentUrl', 'embedUrl', 'transcript'];
            directProps.forEach(prop => {
                if (fields[prop]) schema[prop] = fields[prop];
            });

            if (fields.uploadDate) schema.uploadDate = this.toIsoDate(fields.uploadDate);

            // Author - support dot-notation
            const authorName = fields['author.name'];
            if (authorName) {
                schema.author = {
                    "@type": fields['author.type'] || "Person",
                    "name": authorName
                };
                if (fields['author.url']) schema.author.url = fields['author.url'];
            }

            // Publisher - support dot-notation
            const publisherName = fields['publisher.name'];
            const publisherLogo = fields['publisher.logo'];
            if (publisherName || publisherLogo) {
                schema.publisher = {
                    "@type": "Organization"
                };
                if (publisherName) schema.publisher.name = publisherName;
                if (publisherLogo) {
                    schema.publisher.logo = {
                        "@type": "ImageObject",
                        "url": publisherLogo
                    };
                }
            }

            // Interaction Statistics - support dot-notation
            const viewCount = fields['interactionStatistic.viewCount'] || fields.viewCount;
            if (viewCount) {
                schema.interactionStatistic = {
                    "@type": "InteractionCounter",
                    "interactionType": "https://schema.org/WatchAction",
                    "userInteractionCount": parseInt(viewCount)
                };
            }
        },

        /**
         * Build Review schema with proper structures
         */
        buildReviewSchema: function(schema, fields) {
            const directProps = ['name', 'reviewBody', 'reviewAspect'];
            directProps.forEach(prop => {
                if (fields[prop]) schema[prop] = fields[prop];
            });

            if (fields.datePublished) schema.datePublished = this.toIsoDate(fields.datePublished);

            // Item Reviewed - support dot-notation
            const itemName = fields['itemReviewed.name'] || fields.itemName;
            const itemType = fields['itemReviewed.type'] || fields.itemType;
            if (itemName || itemType) {
                schema.itemReviewed = {
                    "@type": itemType || "Product",
                    "name": itemName || ""
                };
                const itemImage = fields['itemReviewed.image'] || fields.itemImage;
                const itemUrl = fields['itemReviewed.url'] || fields.itemUrl;
                if (itemImage) schema.itemReviewed.image = itemImage;
                if (itemUrl) schema.itemReviewed.url = itemUrl;
            }

            // Review Rating - support dot-notation
            const ratingValue = fields['reviewRating.ratingValue'] || fields.ratingValue;
            if (ratingValue) {
                const bestRating  = fields['reviewRating.bestRating']  || fields.bestRating;
                const worstRating = fields['reviewRating.worstRating'] || fields.worstRating;
                schema.reviewRating = {
                    "@type": "Rating",
                    "ratingValue": parseFloat(ratingValue),
                    "bestRating":  bestRating  ? parseFloat(bestRating)  : 5,
                    "worstRating": worstRating ? parseFloat(worstRating) : 1
                };
            }

            // Author - support dot-notation
            const authorName = fields['author.name'] || fields.authorName;
            if (authorName) {
                schema.author = {
                    "@type": fields['author.type'] || fields.authorType || "Person",
                    "name": authorName
                };
                const authorImage = fields['author.image'] || fields.authorImage;
                const authorUrl = fields['author.url'] || fields.authorUrl;
                if (authorImage) schema.author.image = authorImage;
                if (authorUrl) schema.author.url = authorUrl;
            }

            // Publisher - support dot-notation
            const publisherName = fields['publisher.name'] || fields.publisherName;
            if (publisherName) {
                schema.publisher = {
                    "@type": fields['publisher.type'] || fields.publisherType || "Organization",
                    "name": publisherName
                };
            }
        },

        /**
         * Build Course schema with proper structures
         */
        buildCourseSchema: function(schema, fields) {
            const directProps = ['name', 'description', 'image', 'coursePrerequisites', 'educationalLevel', 'timeRequired', 'inLanguage', 'numberOfCredits'];
            directProps.forEach(prop => {
                if (fields[prop]) schema[prop] = fields[prop];
            });

            // Provider - support dot-notation
            const providerName = fields['provider.name'];
            if (providerName) {
                schema.provider = {
                    "@type": "Organization",
                    "name": providerName
                };
                if (fields['provider.url']) schema.provider.url = fields['provider.url'];
            }

            // Offers - support dot-notation
            const offersPrice = fields['offers.price'] || fields.price;
            const offersCurrency = fields['offers.priceCurrency'] || fields.priceCurrency;
            if (offersPrice !== undefined || offersCurrency) {
                schema.offers = {
                    "@type": "Offer"
                };
                if (offersPrice !== undefined) schema.offers.price = offersPrice;
                if (offersCurrency) schema.offers.priceCurrency = offersCurrency;
                const offersCategory = fields['offers.category'] || fields.category;
                const offersAvailability = fields['offers.availability'] || fields.availability;
                if (offersCategory) schema.offers.category = offersCategory;
                if (offersAvailability) schema.offers.availability = offersAvailability;
                if (fields['offers.url']) schema.offers.url = fields['offers.url'];
                const offersValidFrom = fields['offers.validFrom'] || fields.validFrom;
                if (offersValidFrom) schema.offers.validFrom = offersValidFrom;
            }

            // Aggregate Rating — both ratingValue AND reviewCount required by Google
            const ratingValue = fields['aggregateRating.ratingValue'] || fields.ratingValue;
            const reviewCount = fields['aggregateRating.reviewCount'] || fields.reviewCount;
            if (ratingValue && reviewCount) {
                const bestRatingRaw  = fields['aggregateRating.bestRating']  || fields.bestRating;
                const worstRatingRaw = fields['aggregateRating.worstRating'] || fields.worstRating;
                const bestRating  = (bestRatingRaw  !== undefined && bestRatingRaw  !== null && bestRatingRaw  !== '') ? Math.max(0, parseFloat(bestRatingRaw))  : 5;
                const worstRating = (worstRatingRaw !== undefined && worstRatingRaw !== null && worstRatingRaw !== '') ? Math.max(0, parseFloat(worstRatingRaw)) : 1;
                schema.aggregateRating = {
                    "@type":       "AggregateRating",
                    "ratingValue": parseFloat(ratingValue),
                    "reviewCount": parseInt(reviewCount),
                    "bestRating":  bestRating,
                    "worstRating": worstRating
                };
            }
        },

        /**
         * Build Service schema with proper structures
         */
        buildServiceSchema: function(schema, fields) {
            const directProps = ['name', 'description', 'serviceType', 'image'];
            directProps.forEach(prop => {
                if (fields[prop]) schema[prop] = fields[prop];
            });

            // Provider - support dot-notation
            const providerName = fields['provider.name'];
            if (providerName) {
                schema.provider = {
                    "@type": "Organization",
                    "name": providerName
                };
                if (fields['provider.telephone']) schema.provider.telephone = fields['provider.telephone'];
                if (fields['provider.email']) schema.provider.email = fields['provider.email'];
                if (fields['provider.url']) schema.provider.url = fields['provider.url'];
            }

            // Area Served - support dot-notation (nested group: areaServed.name, areaServed.geo.latitude)
            const areaName = fields['areaServed.name'];
            if (areaName) {
                schema.areaServed = {
                    "@type": "Place",
                    "name": areaName
                };
                const geoLat = fields['areaServed.geo.latitude'];
                const geoLng = fields['areaServed.geo.longitude'];
                if (geoLat && geoLng) {
                    schema.areaServed.geo = {
                        "@type": "GeoCoordinates",
                        "latitude": geoLat,
                        "longitude": geoLng
                    };
                }
            }

            // Offers - support dot-notation
            const offersPrice = fields['offers.price'] || fields.price;
            const offersCurrency = fields['offers.priceCurrency'] || fields.priceCurrency;
            if (offersPrice !== undefined || offersCurrency) {
                schema.offers = {
                    "@type": "Offer"
                };
                if (offersPrice !== undefined) schema.offers.price = offersPrice;
                if (offersCurrency) schema.offers.priceCurrency = offersCurrency;
                const priceSpec = fields['offers.priceSpecification'] || fields.priceSpecification;
                if (priceSpec) {
                    schema.offers.priceSpecification = {
                        "@type": "UnitPriceSpecification",
                        "name": priceSpec
                    };
                }
            }

            // NOTE: Service deliberately does NOT emit aggregateRating/review.
            // Google does not support rating snippets on the Service type — it
            // always reports "<parent_node> type not valid" in the Rich Results
            // test. Customers who want ratings should use LocalBusiness.
        },

        /**
         * Build Person schema with proper structures
         */
        buildPersonSchema: function(schema, fields) {
            const directProps = ['name', 'alternateName', 'description', 'image', 'url', 'jobTitle', 'email', 'telephone',
                                 'birthDate', 'birthPlace', 'nationality', 'gender', 'knowsLanguage', 'alumniOf', 'award',
                                 'honorificPrefix', 'honorificSuffix', 'knowsAbout'];
            directProps.forEach(prop => {
                if (fields[prop]) schema[prop] = fields[prop];
            });

            // Works For - support dot-notation
            const worksForName = fields['worksFor.name'] || fields.orgName;
            if (worksForName) {
                schema.worksFor = {
                    "@type": "Organization",
                    "name": worksForName
                };
                const worksForUrl = fields['worksFor.url'] || fields.orgUrl;
                if (worksForUrl) schema.worksFor.url = worksForUrl;
            }

            // Address - support dot-notation
            const streetAddress = fields['address.streetAddress'] || fields.streetAddress;
            const addressLocality = fields['address.addressLocality'] || fields.addressLocality;
            if (streetAddress || addressLocality) {
                schema.address = {
                    "@type": "PostalAddress"
                };
                if (streetAddress) schema.address.streetAddress = streetAddress;
                if (addressLocality) schema.address.addressLocality = addressLocality;
                const addressRegion = fields['address.addressRegion'] || fields.addressRegion;
                const postalCode = fields['address.postalCode'] || fields.postalCode;
                const addressCountry = fields['address.addressCountry'] || fields.addressCountry;
                if (addressRegion) schema.address.addressRegion = addressRegion;
                if (postalCode) schema.address.postalCode = postalCode;
                if (addressCountry) schema.address.addressCountry = addressCountry;
            }
        },

        /**
         * Build JobPosting schema with proper structures
         */
        buildJobPostingSchema: function(schema, fields) {
            const directProps = ['title', 'description', 'employmentType', 'jobLocationType',
                                 'workHours', 'qualifications',
                                 'responsibilities', 'benefits', 'industry', 'directApply'];
            directProps.forEach(prop => {
                if (fields[prop]) schema[prop] = fields[prop];
            });
            // Date fields — normalize to ISO 8601 with timezone (Google requires it)
            if (fields.datePosted)    schema.datePosted    = this.toIsoDate(fields.datePosted);
            if (fields.validThrough)  schema.validThrough  = this.toIsoDate(fields.validThrough);

            // experienceRequirements - must be OccupationalExperienceRequirements object
            const expMonths = fields['experienceRequirements.monthsOfExperience'];
            if (expMonths) {
                schema.experienceRequirements = {
                    "@type": "OccupationalExperienceRequirements",
                    "monthsOfExperience": parseFloat(expMonths)
                };
            }

            // educationRequirements - must be EducationalOccupationalCredential object
            const eduCategory = fields['educationRequirements.credentialCategory'];
            if (eduCategory) {
                schema.educationRequirements = {
                    "@type": "EducationalOccupationalCredential",
                    "credentialCategory": eduCategory
                };
            }

            // applicantLocationRequirements - must be AdministrativeArea object (Country/State/City)
            const applLocType = fields['applicantLocationRequirements.type'];
            const applLocName = fields['applicantLocationRequirements.name'];
            if (applLocName) {
                schema.applicantLocationRequirements = {
                    "@type": applLocType || "Country",
                    "name": applLocName
                };
            } else if (fields['jobLocationType'] === 'TELECOMMUTE') {
                // Google requires applicantLocationRequirements when jobLocationType is TELECOMMUTE.
                // Default to Worldwide so the schema passes Rich Results validation.
                schema.applicantLocationRequirements = {
                    "@type": "Country",
                    "name": "Worldwide"
                };
            }

            // Hiring Organization - support dot-notation
            const hiringOrgName = fields['hiringOrganization.name'];
            if (hiringOrgName) {
                schema.hiringOrganization = {
                    "@type": "Organization",
                    "name": hiringOrgName
                };
                if (fields['hiringOrganization.sameAs']) schema.hiringOrganization.sameAs = fields['hiringOrganization.sameAs'];
                if (fields['hiringOrganization.logo']) schema.hiringOrganization.logo = fields['hiringOrganization.logo'];
            }

            // Job Location - support dot-notation
            const jobLocLocality = fields['jobLocation.addressLocality'] || fields.addressLocality;
            const jobLocCountry = fields['jobLocation.addressCountry'] || fields.addressCountry;
            if (jobLocLocality || jobLocCountry) {
                schema.jobLocation = {
                    "@type": "Place",
                    "address": {
                        "@type": "PostalAddress"
                    }
                };
                const jobLocStreet = fields['jobLocation.streetAddress'] || fields.streetAddress;
                const jobLocRegion = fields['jobLocation.addressRegion'] || fields.addressRegion;
                const jobLocPostal = fields['jobLocation.postalCode'] || fields.postalCode;
                if (jobLocStreet) schema.jobLocation.address.streetAddress = jobLocStreet;
                if (jobLocLocality) schema.jobLocation.address.addressLocality = jobLocLocality;
                if (jobLocRegion) schema.jobLocation.address.addressRegion = jobLocRegion;
                if (jobLocPostal) schema.jobLocation.address.postalCode = jobLocPostal;
                if (jobLocCountry) schema.jobLocation.address.addressCountry = jobLocCountry;
            }

            // Base Salary - support dot-notation
            const salaryMin = fields['baseSalary.minValue'] || fields.minValue;
            const salaryMax = fields['baseSalary.maxValue'] || fields.maxValue;
            if (salaryMin || salaryMax) {
                schema.baseSalary = {
                    "@type": "MonetaryAmount",
                    "currency": fields['baseSalary.currency'] || fields.currency || "USD",
                    "value": {
                        "@type": "QuantitativeValue"
                    }
                };
                if (salaryMin) schema.baseSalary.value.minValue = salaryMin;
                if (salaryMax) schema.baseSalary.value.maxValue = salaryMax;
                const unitText = fields['baseSalary.unitText'] || fields.unitText;
                if (unitText) schema.baseSalary.value.unitText = unitText;
            }

            // Application Contact - support dot-notation
            if (fields['applicationContact.email'] || fields['applicationContact.telephone'] || fields['applicationContact.url']) {
                schema.applicationContact = {
                    "@type": "ContactPoint"
                };
                if (fields['applicationContact.email']) schema.applicationContact.email = fields['applicationContact.email'];
                if (fields['applicationContact.telephone']) schema.applicationContact.telephone = fields['applicationContact.telephone'];
                if (fields['applicationContact.url']) schema.applicationContact.url = fields['applicationContact.url'];
            }
        },

        /**
         * Build WebPage schema with proper structures
         */
        buildWebPageSchema: function(schema, fields) {
            const directProps = ['name', 'description', 'url', 'image', 'primaryImageOfPage', 'datePublished', 'dateModified',
                                 'inLanguage', 'keywords', 'mainEntity'];
            directProps.forEach(prop => {
                if (fields[prop]) schema[prop] = fields[prop];
            });

            // Author - support dot-notation
            const authorName = fields['author.name'] || fields.authorName;
            if (authorName) {
                schema.author = {
                    "@type": fields['author.type'] || fields.authorType || "Organization",
                    "name": authorName
                };
                const authorUrl = fields['author.url'] || fields.authorUrl;
                if (authorUrl) schema.author.url = authorUrl;
            }

            // Publisher - support dot-notation
            const publisherName = fields['publisher.name'] || fields.publisherName;
            if (publisherName) {
                schema.publisher = {
                    "@type": "Organization",
                    "name": publisherName
                };
                const publisherLogo = fields['publisher.logo'] || fields.publisherLogo;
                if (publisherLogo) {
                    schema.publisher.logo = {
                        "@type": "ImageObject",
                        "url": publisherLogo
                    };
                }
            }

            // Speakable - support dot-notation
            const cssSelector = fields['speakable.cssSelector'] || fields.cssSelector;
            const xpath = fields['speakable.xpath'] || fields.xpath;
            if (cssSelector || xpath) {
                schema.speakable = {
                    "@type": "SpeakableSpecification"
                };
                if (cssSelector) schema.speakable.cssSelector = cssSelector.split(',').map(s => s.trim());
                if (xpath) schema.speakable.xpath = xpath.split(',').map(s => s.trim());
            }
        },

        /**
         * Copy JSON preview to clipboard
         */
        copyJson: function() {
            const json = $('#schemafy-json-preview code').text();
            navigator.clipboard.writeText(json).then(() => {
                window.SchemafyUtilities.showNotification(t('json_copied', 'JSON copied to clipboard!'), 'success');
            }).catch(() => {
                window.SchemafyUtilities.showNotification(t('failed_to_copy', 'Failed to copy'), 'error');
            });
        },

        cloneJson: function(value) {
            if (value === undefined || value === null) {
                return value;
            }
            try {
                return JSON.parse(JSON.stringify(value));
            } catch (e) {
                return value;
            }
        },

        mergeEditedSchema: function(originalSchema, freshSchema) {
            const original = this.cloneJson(originalSchema) || {};
            const fresh = this.cloneJson(freshSchema) || {};
            const merged = this.deepMergeSchema(original, fresh);

            if (original['@type']) {
                merged['@type'] = original['@type'];
            }
            if (original['@id'] && !fresh['@id']) {
                merged['@id'] = original['@id'];
            }

            return merged;
        },

        deepMergeSchema: function(original, fresh) {
            if (!original || typeof original !== 'object' || Array.isArray(original)) {
                return fresh;
            }

            const merged = this.cloneJson(original) || {};
            Object.keys(fresh || {}).forEach((key) => {
                const freshValue = fresh[key];
                const originalValue = original[key];

                if (freshValue === undefined || freshValue === null || freshValue === '') {
                    return;
                }

                if (Array.isArray(freshValue)) {
                    if (freshValue.length > 0) {
                        merged[key] = freshValue;
                    }
                    return;
                }

                if (originalValue && typeof originalValue === 'object' && !Array.isArray(originalValue) &&
                    freshValue && typeof freshValue === 'object' && !Array.isArray(freshValue)) {
                    merged[key] = this.deepMergeSchema(originalValue, freshValue);
                    return;
                }

                if (Array.isArray(originalValue) && typeof freshValue === 'string' && originalValue[0] === freshValue) {
                    return;
                }

                merged[key] = freshValue;
            });

            return merged;
        },

        /**
         * Variables resolvable by the frontend injector. Anything else inside
         * {{...}} is a typo that would be published literally in the JSON-LD.
         */
        knownVariables: [
            'meta_title', 'meta_description', 'site_name', 'site_url', 'current_url',
            'site_description', 'site_language', 'current_year',
            'post_title', 'post_date', 'post_modified', 'post_excerpt', 'post_id',
            'post_type', 'post_author', 'post_url', 'author_name', 'featured_image',
            'category', 'tags',
            'product_price', 'product_regular_price', 'product_sale_price',
            'product_currency', 'product_sku', 'product_stock_status',
            'product_short_description', 'product_weight', 'product_category',
            'product_rating', 'product_review_count', 'product_image',
        ],

        /**
         * Validate a generated JSON-LD object against Schema.org / Google
         * Rich Results requirements before saving.
         *
         * Returns { errors: [], warnings: [] }. Errors produce schemas Google
         * rejects outright and block the save; warnings are recommendations.
         */
        validateSchemaData: function(schema) {
            const self = this;
            const errors = [];
            const warnings = [];

            const URL_KEYS = ['url', 'image', 'logo', 'thumbnailUrl', 'contentUrl', 'embedUrl', 'sameAs'];
            const DURATION_KEYS = this.durationFieldNames.concat(['duration']);
            const TIME_RE = /^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/;
            const DURATION_RE = /^PT(?=\d)(\d+H)?(\d+M)?(\d+S)?$/i;
            const VAR_RE = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;

            const isVariable = function(value) {
                return typeof value === 'string' && /^\{\{[a-zA-Z0-9_\s]+\}\}$/.test(value.trim());
            };

            const checkUrlValue = function(key, value) {
                if (typeof value !== 'string' || value === '' || isVariable(value)) return;
                if (!/^https?:\/\//i.test(value.trim())) {
                    errors.push('"' + key + '": "' + value.slice(0, 60) + '" ' +
                        t('val_url_absolute', 'must be an absolute URL starting with https://'));
                }
            };

            const walk = function(node, path) {
                if (Array.isArray(node)) {
                    node.forEach(function(item, i) { walk(item, path + '[' + i + ']'); });
                    return;
                }
                if (!node || typeof node !== 'object') {
                    // Variable typo check on plain strings
                    if (typeof node === 'string') {
                        let m;
                        while ((m = VAR_RE.exec(node)) !== null) {
                            if (self.knownVariables.indexOf(m[1]) === -1) {
                                errors.push('{{' + m[1] + '}} ' +
                                    t('val_unknown_variable', 'is not a known variable — it would be published literally. Check the spelling.'));
                            }
                        }
                    }
                    return;
                }

                const type = node['@type'];

                // ── Ratings ─────────────────────────────────────────────
                if (type === 'Rating' || type === 'AggregateRating') {
                    const rv = parseFloat(node.ratingValue);
                    const best = node.bestRating !== undefined ? parseFloat(node.bestRating) : 5;
                    const worst = node.worstRating !== undefined ? parseFloat(node.worstRating) : 1;

                    // worstRating/bestRating sanity: no negatives, best must beat worst
                    if (!isNaN(worst) && worst < 0) {
                        errors.push(t('val_worst_negative', 'worstRating cannot be negative (use 0 or 1).'));
                    }
                    if (!isNaN(best) && !isNaN(worst) && best <= worst) {
                        errors.push(t('val_best_worst', 'bestRating must be greater than worstRating.'));
                    }
                    if (!isNaN(rv)) {
                        if (rv < worst || rv > best) {
                            errors.push(t('val_rating_range', 'Rating value') + ' ' + rv + ' ' +
                                t('val_rating_range_2', 'is outside the allowed range') + ' (' + worst + '–' + best + ')');
                        }
                    }
                    if (type === 'AggregateRating') {
                        const count = parseInt(node.reviewCount !== undefined ? node.reviewCount : node.ratingCount, 10);
                        if (!isNaN(count) && count < 1) {
                            errors.push(t('val_review_count', 'Review count must be at least 1'));
                        }
                    }
                }

                // ── VideoObject completeness ────────────────────────────
                // A partial VideoObject (e.g. only contentUrl) makes Recipe/HowTo
                // fail Google with "missing uploadDate/name/thumbnailUrl".
                if (type === 'VideoObject') {
                    ['name', 'uploadDate', 'thumbnailUrl'].forEach(function(reqProp) {
                        if (!node[reqProp]) {
                            errors.push(t('val_video_field', 'The video is missing the required field') + ' "' + reqProp + '"');
                        }
                    });
                    if (!node.contentUrl && !node.embedUrl) {
                        errors.push(t('val_video_url', 'The video needs a contentUrl or embedUrl.'));
                    }
                }

                // ── Offers ──────────────────────────────────────────────
                if (type === 'Offer') {
                    const hasPrice = node.price !== undefined && node.price !== '';
                    const hasCurrency = node.priceCurrency !== undefined && node.priceCurrency !== '';
                    if (hasPrice && !hasCurrency) {
                        errors.push(t('val_offer_currency', 'The offer has a price but no currency — Google requires both together.'));
                    }
                    if (!hasPrice && (hasCurrency || node.availability)) {
                        errors.push(t('val_offer_price', 'The offer is missing its price — Google requires price and currency together.'));
                    }
                    if (hasCurrency && !isVariable(node.priceCurrency) && !/^[A-Z]{3}$/.test(String(node.priceCurrency).trim())) {
                        errors.push('"' + node.priceCurrency + '" ' +
                            t('val_currency_iso', 'is not a valid ISO 4217 currency code (use USD, EUR, COP…)'));
                    }
                }

                // ── Opening hours ───────────────────────────────────────
                if (type === 'OpeningHoursSpecification') {
                    ['opens', 'closes'].forEach(function(k) {
                        if (node[k] && !TIME_RE.test(String(node[k]).trim())) {
                            errors.push('"' + k + ': ' + node[k] + '" ' +
                                t('val_time_format', 'must use 24h HH:MM format (e.g. 09:00, 18:30)'));
                        }
                    });
                }

                Object.keys(node).forEach(function(key) {
                    const value = node[key];

                    // ── Dot-notation leak ───────────────────────────────
                    // schema.org has no property names containing ".". A key
                    // like "author.type" means a nested object failed to expand
                    // (seen on edited FAQ/Organization schemas) and Google/schema.org
                    // reject it as an unrecognized property.
                    if (key.indexOf('.') !== -1) {
                        errors.push('"' + key + '" ' +
                            t('val_dotted_key', 'is a malformed property (the nested object did not expand). Re-open and re-save this field.'));
                    }

                    // ── Absolute URLs ───────────────────────────────────
                    if (URL_KEYS.indexOf(key) !== -1) {
                        if (Array.isArray(value)) {
                            value.forEach(function(v) { checkUrlValue(key, v); });
                        } else if (typeof value === 'string') {
                            checkUrlValue(key, value);
                        }
                    }

                    // ── ISO durations ───────────────────────────────────
                    if (DURATION_KEYS.indexOf(key) !== -1 && typeof value === 'string' && value !== '' && !isVariable(value)) {
                        if (!DURATION_RE.test(value.trim())) {
                            errors.push('"' + key + ': ' + value + '" ' +
                                t('val_duration_format', 'is not a valid ISO 8601 duration (expected e.g. PT30M or PT1H30M)'));
                        }
                    }

                    // ── GTIN ────────────────────────────────────────────
                    if (/^gtin(8|12|13|14)?$/.test(key) && value !== '' && !isVariable(value)) {
                        const digits = String(value).trim();
                        if (!/^\d{8}$|^\d{12,14}$/.test(digits)) {
                            errors.push('"' + key + ': ' + value + '" ' +
                                t('val_gtin_format', 'must be 8, 12, 13 or 14 digits with no spaces or dashes'));
                        }
                    }

                    // ── Telephone (warning only) ────────────────────────
                    if (key === 'telephone' && typeof value === 'string' && value !== '' &&
                        !isVariable(value) && value.trim().charAt(0) !== '+') {
                        warnings.push(t('val_phone_e164', 'Phone numbers work best in international format (+57 601 1234567)'));
                    }

                    walk(value, path ? path + '.' + key : key);
                });

                // ── Date order ──────────────────────────────────────────
                const pairs = [['startDate', 'endDate'], ['datePosted', 'validThrough'], ['datePublished', 'dateModified']];
                pairs.forEach(function(pair) {
                    const a = node[pair[0]];
                    const b = node[pair[1]];
                    if (typeof a === 'string' && typeof b === 'string' && a && b && !isVariable(a) && !isVariable(b)) {
                        const da = Date.parse(a);
                        const db = Date.parse(b);
                        if (!isNaN(da) && !isNaN(db) && db < da) {
                            errors.push('"' + pair[1] + '" (' + b.split('T')[0] + ') ' +
                                t('val_date_order', 'cannot be earlier than') + ' "' + pair[0] + '" (' + a.split('T')[0] + ')');
                        }
                    }
                });
            };

            walk(schema, '');

            // ── Top-level type requirements (Google hard requirements) ──
            const topType = schema['@type'];
            const req = function(prop, label) {
                const v = schema[prop];
                const empty = v === undefined || v === null || v === '' || (Array.isArray(v) && v.length === 0);
                if (empty) {
                    errors.push('"' + topType + '" ' + t('val_requires', 'requires') + ' ' + label);
                }
            };

            // Every supported type needs a human-readable name/headline. Without
            // this, the builder happily saved "{@type: Person}" with nothing else.
            var NAME_BY_TYPE = {
                Article: 'headline', NewsArticle: 'headline', BlogPosting: 'headline',
            };
            var TYPES_NEEDING_NAME = [
                'Product', 'Person', 'Organization', 'LocalBusiness', 'Service',
                'Recipe', 'Course', 'Event', 'VideoObject', 'HowTo', 'JobPosting',
                'Article', 'NewsArticle', 'BlogPosting', 'SoftwareApplication', 'Book',
            ];
            if (TYPES_NEEDING_NAME.indexOf(topType) !== -1) {
                var nameProp = NAME_BY_TYPE[topType] || 'name';
                // JobPosting uses "title", not "name"/"headline"
                if (topType === 'JobPosting') nameProp = 'title';
                req(nameProp, '"' + nameProp + '"');
            }

            if (topType === 'Event') {
                req('startDate', '"startDate"');
                req('location', '"location"');
            }
            if (topType === 'Review') {
                req('itemReviewed', '"itemReviewed" (' + t('val_item_reviewed', 'what is being reviewed') + ')');
                req('author', '"author"');
            }
            if (topType === 'FAQPage') {
                req('mainEntity', t('val_faq_questions', 'at least one question and answer'));
            }
            if (topType === 'JobPosting') {
                req('datePosted', '"datePosted"');
                req('hiringOrganization', '"hiringOrganization"');
            }
            if (topType === 'Product') {
                // Google: a Product needs offers, review, OR aggregateRating to be eligible
                if (!schema.offers && !schema.review && !schema.aggregateRating) {
                    errors.push(t('val_product_needs', 'A Product needs at least one of: offers, review or aggregateRating.'));
                }
            }
            if (topType === 'Recipe') {
                req('image', '"image"');
            }

            // ── Review snippet host check ────────────────────────────────
            // Google only renders aggregateRating/review on a fixed set of host
            // types. "Service" is NOT one of them — emitting it always yields the
            // "<parent_node> type not valid" error in the Rich Results test.
            var REVIEW_HOST_OK = [
                'Product', 'LocalBusiness', 'Organization', 'Recipe', 'Event',
                'Book', 'Course', 'CreativeWorkSeason', 'CreativeWorkSeries',
                'Episode', 'Game', 'HowTo', 'MediaObject', 'Movie',
                'MusicPlaylist', 'MusicRecording', 'SoftwareApplication',
            ];
            if ((schema.aggregateRating || schema.review) &&
                REVIEW_HOST_OK.indexOf(topType) === -1) {
                errors.push('"' + topType + '" ' +
                    t('val_review_host', 'does not support rating/review snippets in Google. Use LocalBusiness or Product for ratings.'));
            }

            // De-duplicate repeated messages (arrays can repeat the same issue)
            return {
                errors: errors.filter(function(e, i) { return errors.indexOf(e) === i; }),
                warnings: warnings.filter(function(w, i) { return warnings.indexOf(w) === i; }),
            };
        },

        /**
         * Save builder schema (supports single and multi-URL)
         */
        saveBuilderSchema: function() {
            if (this.isSavingSchema) {
                return;
            }

            const $form = $('#schemafy-dynamic-schema-form');
            if ($form.length === 0) {
                window.SchemafyUtilities.showNotification(t('form_not_found', 'Form not found'), 'error');
                return;
            }

            const schemaName = $form.find('[name="schema_name"]').val();
            const schemaType = $form.find('[name="schema_type"]').val();

            if (!schemaName) {
                window.SchemafyUtilities.showNotification(t('fill_schema_name', 'Please fill Schema Name'), 'error');
                return;
            }

            // Always regenerate fresh JSON from current form state (fixes stale-preview save bug)
            const formData = this.getFormData($form);
            const buildHint = $form.data('schema-builder-type');
            let freshSchema = this.generateSchemaFromForm(schemaType, formData, buildHint);

            // When editing AI-generated schemas, preserve unsupported/unrendered
            // JSON-LD properties while still applying the fields the user changed.
            const editingId = $form.data('editing-id');
            const originalSchema = $form.data('original-schema-data');
            if (editingId && originalSchema && typeof originalSchema === 'object') {
                freshSchema = this.mergeEditedSchema(originalSchema, freshSchema);
            }

            // Validate against Schema.org / Google requirements before saving
            const validation = this.validateSchemaData(freshSchema);
            if (validation.errors.length > 0) {
                const maxShown = 4;
                let message = t('val_fix_before_save', 'Fix these issues before saving:') + ' ' +
                    validation.errors.slice(0, maxShown).join(' • ');
                if (validation.errors.length > maxShown) {
                    message += ' (+' + (validation.errors.length - maxShown) + ')';
                }
                window.SchemafyUtilities.showNotification(message, 'error');
                return;
            }
            if (validation.warnings.length > 0) {
                window.SchemafyUtilities.showNotification(validation.warnings[0], 'info');
            }

            const json = JSON.stringify(freshSchema, null, 2);
            // Keep preview in sync
            $('#schemafy-json-preview code').text(json);

            // Check if we're editing an existing schema
            if (editingId) {
                // Editing mode: use single URL from chips (first one)
                const urls = this.getSelectedUrls();
                const url = urls.length > 0 ? urls[0] : '';
                if (!url) {
                    window.SchemafyUtilities.showNotification(t('add_target_url', 'Please add a Target URL'), 'error');
                    return;
                }
                this.updateExistingSchema(editingId, url, schemaName, schemaType, json);
                return;
            }

            // New schema: get URLs from chip list
            const urls = this.getSelectedUrls();

            if (urls.length === 0) {
                window.SchemafyUtilities.showNotification(t('add_one_target_url', 'Please add at least one Target URL'), 'error');
                return;
            }

            if (urls.length === 1) {
                // Single URL: use existing flow with duplicate detection
                const url = urls[0];
                if (window.SchemafyOperations) {
                    window.SchemafyOperations.checkExistingSchema(url, schemaType, (existingSchemas) => {
                        if (existingSchemas && existingSchemas.length > 0) {
                            window.SchemafyOperations.handleDuplicateSchema(url, schemaType, schemaName, existingSchemas, formData, json);
                        } else {
                            window.SchemafyOperations.proceedToSaveSchema(url, schemaName, schemaType, formData, json);
                        }
                    });
                }
            } else {
                // Multiple URLs: use bulk save
                if (window.SchemafyOperations) {
                    window.SchemafyOperations.proceedToSaveSchemaBulk(urls, schemaName, schemaType, formData, json);
                }
            }
        },

        /**
         * Update an existing schema
         */
        updateExistingSchema: function(id, url, schemaName, schemaType, json) {
            const self = this;
            const $saveBtn = $('#schemafy-save-builder-schema');
            const originalText = $saveBtn.length ? $saveBtn.html() : '';

            if ($saveBtn.length) {
                $saveBtn.prop('disabled', true).html('<span class="dashicons dashicons-update-alt schemafy-spin"></span> Updating...');
            }

            $.ajax({
                url: schemafy_ajax.ajax_url,
                type: 'POST',
                data: {
                    action: 'schemafy_update_schema',
                    nonce: schemafy_ajax.nonce,
                    id: id,
                    url: url,
                    schema_name: schemaName,
                    schema_type: schemaType,
                    schema_data: json,
                    meta_title: '',
                    meta_description: ''
                },
                success: function(response) {
                    if (response && response.success) {
                        window.SchemafyUtilities.showNotification(t('schema_updated', 'Schema updated successfully!'), 'success');

                        // Clear the editing state
                        $('#schemafy-dynamic-schema-form').removeData('editing-id');
                        $('#schemafy-dynamic-schema-form').removeData('original-schema-data original-schema-type storage-schema-type schema-builder-type');

                        // Reset save button to original state
                        if (window.SchemafyOperations && typeof window.SchemafyOperations.resetSaveButton === 'function') {
                            window.SchemafyOperations.resetSaveButton();
                        }

                        setTimeout(function() {
                            const $form = $('#schemafy-dynamic-schema-form');
                            if ($form.length && $form[0]) {
                                $form[0].reset();
                            }

                            // Clear URL chips and restore URL controls
                            $('#schemafy-selected-urls').empty();
                            $('#schemafy-target-urls').val('[]');
                            $('#schemafy-url-combobox').val('').prop('disabled', false);
                            $('#schemafy-url-results').hide();

                            $('.schemafy-schema-type-card').removeClass('active');
                            $('#schemafy-builder-form-container').html(`
                                <div class="schemafy-builder-placeholder">
                                    <span class="schemafy-builder-placeholder-emoji">🎉</span>
                                    <h3>${t('schema_updated', 'Schema Applied Successfully!')}</h3>
                                    <p>${t('select_another_type', 'Google will show it the next time it crawls your page. Pick another type to keep adding.')}</p>
                                </div>
                            `);
                            $('#schemafy-json-preview code').text('{\n  "@context": "https://schema.org",\n  "@type": "...",\n  "name": "..."\n}');

                            // Reset step indicator and friendly preview to initial state
                            if (window.SchemafyBuilder && typeof window.SchemafyBuilder.updateStepIndicator === 'function') {
                                window.SchemafyBuilder.updateStepIndicator(1);
                            }
                            $('#schemafy-friendly-type, #schemafy-friendly-url, #schemafy-friendly-fields').text('–');
                            $('#schemafy-friendly-rows').hide();
                            $('.schemafy-preview-friendly-hint').show();
                            $('#schemafy-preview-body').hide();
                            $('#schemafy-toggle-json-preview .schemafy-toggle-arrow').text('▼');
                            $('#schemafy-toggle-json-preview').attr('aria-expanded', 'false');

                            // Refresh dashboard and bulk manager if available
                            if (window.SchemafyDashboard && typeof window.SchemafyDashboard.refreshStats === 'function') {
                                window.SchemafyDashboard.refreshStats();
                            }
                            if (window.SchemafyBulk && typeof window.SchemafyBulk.loadBulkSchemas === 'function') {
                                window.SchemafyBulk.loadBulkSchemas();
                            }
                        }, 500);
                    } else {
                        window.SchemafyUtilities.showNotification((response && response.data) || t('error_updating', 'Error updating schema'), 'error');
                        if ($saveBtn.length) {
                            $saveBtn.prop('disabled', false).html(originalText);
                        }
                    }
                },
                error: function() {
                    window.SchemafyUtilities.showNotification(t('network_error_updating', 'Network error: Could not update schema'), 'error');
                    if ($saveBtn.length) {
                        $saveBtn.prop('disabled', false).html(originalText);
                    }
                }
            });
        }
    };

})(jQuery);
