/**
 * TamRank Schema Builder
 * Handles tab navigation, template wizard, AJAX operations, and global settings
 */
(function ($) {
    'use strict';

    /**
     * Fill one placeholder without letting the value expand.
     *
     * Copy of the canonical helper in assets/js/tamrank-onboarding.js; read the
     * docblock there for why String.replace() with a STRING replacement mangles
     * any value carrying $&, $`, $' or $$, and why this is a copy instead of a
     * shared global. `token` may be a string or a RegExp; a FUNCTION replacement
     * is exempt from that expansion in both cases. Keep the body identical.
     */
    function fillPlaceholder(tpl, token, value) {
        var literal = String(value);
        return String(tpl).replace(token, function () { return literal; });
    }

    var SVG_CHECK = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';

    var SchemaBuilder = {
        nonce: '',
        ajaxUrl: '',
        fieldDefs: {},
        fieldSources: {},
        existingTemplates: {},
        selectedSchemaType: '',
        editingTemplateId: null,

        init: function () {
            var wrap = document.querySelector('.tr-schema-wrap');
            if (!wrap) return;

            this.nonce = wrap.dataset.nonce;
            this.ajaxUrl = window.tamRank ? window.tamRank.ajaxUrl : ajaxurl;

            // Data is provided via wp_add_inline_script (window.tamrankSchemaData) instead
            // of inline <script type="application/json"> data islands (wp.org compliance).
            var schemaData = window.tamrankSchemaData || {};
            this.fieldDefs = schemaData.fieldDefs || {};
            this.fieldSources = schemaData.fieldSources || {};
            this.existingTemplates = schemaData.existingTemplates || {};

            this.bindTabs();
            this.bindTestUrl();
            this.bindTemplateList();
            this.bindTemplateWizard();
            this.bindGlobalSettings();
        },

        parseJSON: function (selector) {
            var el = document.querySelector(selector);
            if (!el) return {};
            try {
                return JSON.parse(el.textContent);
            } catch (e) {
                return {};
            }
        },

        /* ========================================
         * TAB NAVIGATION
         * ======================================== */
        bindTabs: function () {
            $(document).on('click', '.tr-pill-nav .tr-pill[data-tab]', function () {
                var tab = $(this).data('tab');

                $('.tr-pill-nav .tr-pill').removeClass('is-active');
                $(this).addClass('is-active');

                $('.tr-tab-panel[data-panel]').removeClass('is-active');
                $('.tr-tab-panel[data-panel="' + tab + '"]').addClass('is-active');
            });

            // Activate tab from URL parameter (?tab=instellingen)
            var params = new URLSearchParams(window.location.search);
            var urlTab = params.get('tab');
            if (urlTab && $('.tr-pill[data-tab="' + urlTab + '"]').length) {
                $('.tr-pill[data-tab="' + urlTab + '"]').trigger('click');
            }
        },

        /* ========================================
         * TEST URL (Google Rich Results)
         * ======================================== */
        bindTestUrl: function () {
            $(document).on('click', '#tr-test-google-btn', function () {
                var url = $('#tr-test-url-input').val().trim();
                if (!url) {
                    TamRankToast.show(tamrankSchemaBuilder.strings.fill_url, '', 'warning');
                    return;
                }
                window.open(
                    'https://search.google.com/test/rich-results?url=' + encodeURIComponent(url),
                    '_blank'
                );
            });

            // Allow Enter key on the input
            $(document).on('keydown', '#tr-test-url-input', function (e) {
                if (e.key === 'Enter') {
                    e.preventDefault();
                    $('#tr-test-google-btn').trigger('click');
                }
            });
        },

        /* ========================================
         * TEMPLATE LIST
         * ======================================== */
        bindTemplateList: function () {
            var self = this;

            // New template button
            $(document).on('click', '#tr-new-template-btn', function () {
                self.resetTemplateForm();
                self.showTemplateForm();
            });

            // Back to list
            $(document).on('click', '#tr-back-to-list', function (e) {
                e.preventDefault();
                self.hideTemplateForm();
            });

            // Edit template
            $(document).on('click', '.tr-edit-template-btn', function () {
                var id = $(this).data('id');
                self.loadTemplateForEdit(id);
            });

            // Delete template
            $(document).on('click', '.tr-delete-template-btn', function () {
                var id = $(this).data('id');
                window.TamRank.confirm({
                    title: tamrankSchemaBuilder.strings.delete_template_title || 'Delete template?',
                    message: tamrankSchemaBuilder.strings.confirm_delete_template,
                    confirmLabel: tamrankSchemaBuilder.strings.delete || 'Delete',
                    variant: 'danger'
                }).then(function (ok) {
                    if (ok) self.deleteTemplate(id);
                });
            });
        },

        showTemplateForm: function () {
            $('#tr-templates-list').hide();
            $('#tr-template-form').show();
        },

        hideTemplateForm: function () {
            $('#tr-template-form').hide();
            $('#tr-templates-list').show();
            this.resetTemplateForm();
        },

        resetTemplateForm: function () {
            this.editingTemplateId = null;
            this.selectedSchemaType = '';
            $('#tr-edit-template-id').val('');
            $('#tr-tpl-name').val('');

            // Reset schema type cards
            $('.tr-schema-type-card').removeClass('is-selected');
            $('#tr-more-types-select').val('');

            // Reset post type checkboxes
            $('#tr-post-types-checkboxes input[type="checkbox"]').prop('checked', false);

            // Reset wizard to step 1
            this.goToStep(1);

            // Clear field mapping areas
            $('#tr-required-fields').empty();
            $('#tr-optional-fields').empty();
        },

        loadTemplateForEdit: function (id) {
            var template = this.existingTemplates[id];
            if (!template) {
                TamRankToast.show(tamrankSchemaBuilder.strings.template_not_found, '', 'error');
                return;
            }

            this.resetTemplateForm();
            this.editingTemplateId = id;
            $('#tr-edit-template-id').val(id);
            $('#tr-tpl-name').val(template.name);

            // Set schema type
            this.selectSchemaType(template.schema_type);

            // Set post types
            if (template.post_types && template.post_types.length) {
                template.post_types.forEach(function (pt) {
                    $('#tr-post-types-checkboxes input[value="' + pt + '"]').prop('checked', true);
                });
            }

            this.showTemplateForm();
        },

        selectSchemaType: function (type) {
            this.selectedSchemaType = type;

            // Clear all selections
            $('.tr-schema-type-card').removeClass('is-selected');
            $('#tr-more-types-select').val('');

            // Check if it's a featured card
            var card = $('.tr-schema-type-card[data-type="' + type + '"]');
            if (card.length) {
                card.addClass('is-selected');
            } else {
                // Set dropdown value
                $('#tr-more-types-select').val(type);
            }
        },

        /* ========================================
         * TEMPLATE WIZARD
         * ======================================== */
        bindTemplateWizard: function () {
            var self = this;

            // Schema type card selection
            $(document).on('click', '.tr-schema-type-card', function () {
                $('.tr-schema-type-card').removeClass('is-selected');
                $(this).addClass('is-selected');
                self.selectedSchemaType = $(this).data('type');
                $('#tr-more-types-select').val('');
            });

            // More types dropdown
            $(document).on('change', '#tr-more-types-select', function () {
                var val = $(this).val();
                if (val) {
                    $('.tr-schema-type-card').removeClass('is-selected');
                    self.selectedSchemaType = val;
                }
            });

            // Step 1 → Step 2
            $(document).on('click', '#tr-to-step2-btn', function () {
                if (!self.validateStep1()) return;
                self.renderFieldMappings();
                self.goToStep(2);
            });

            // Step 2 → Step 1
            $(document).on('click', '#tr-to-step1-btn', function () {
                self.goToStep(1);
            });

            // Cancel
            $(document).on('click', '#tr-cancel-step1', function () {
                self.hideTemplateForm();
            });

            // Save template
            $(document).on('click', '#tr-save-template-btn', function () {
                self.saveTemplate();
            });
        },

        validateStep1: function () {
            var name = $('#tr-tpl-name').val().trim();
            if (!name) {
                TamRankToast.show(tamrankSchemaBuilder.strings.fill_template_name, '', 'warning');
                $('#tr-tpl-name').focus();
                return false;
            }
            if (!this.selectedSchemaType) {
                TamRankToast.show(tamrankSchemaBuilder.strings.choose_schema_type, '', 'warning');
                return false;
            }
            var checked = $('#tr-post-types-checkboxes input[type="checkbox"]:checked');
            if (checked.length === 0) {
                TamRankToast.show(tamrankSchemaBuilder.strings.select_post_type, '', 'warning');
                return false;
            }
            return true;
        },

        goToStep: function (step) {
            if (step === 1) {
                $('#tr-wizard-step-1').show();
                $('#tr-wizard-step-2').hide();
                $('#tr-step-circle-1').addClass('is-active').removeClass('is-inactive');
                $('#tr-step-circle-2').addClass('is-inactive').removeClass('is-active');
                $('#tr-step-label-1').removeClass('is-inactive');
                $('#tr-step-label-2').addClass('is-inactive');
                $('#tr-step-connector').removeClass('is-complete');
            } else {
                $('#tr-wizard-step-1').hide();
                $('#tr-wizard-step-2').show();
                $('#tr-step-circle-1').addClass('is-active').removeClass('is-inactive');
                $('#tr-step-circle-2').addClass('is-active').removeClass('is-inactive');
                $('#tr-step-label-1').removeClass('is-inactive');
                $('#tr-step-label-2').removeClass('is-inactive');
                $('#tr-step-connector').addClass('is-complete');
            }
        },

        renderFieldMappings: function () {
            var type = this.selectedSchemaType;
            var defs = this.fieldDefs[type];

            if (!defs) {
                $('#tr-required-fields').html(
                    '<p class="tr-text-muted">' + (tamrankSchemaBuilder.strings.no_field_config || 'No field configuration available for this type.') + '</p>'
                );
                $('#tr-optional-fields').empty();
                return;
            }

            // Get existing mappings if editing
            var existingMappings = {};
            if (this.editingTemplateId && this.existingTemplates[this.editingTemplateId]) {
                existingMappings = this.existingTemplates[this.editingTemplateId].field_mappings || {};
            }

            $('#tr-required-fields').html(this.buildFieldRows(defs.required, existingMappings));
            $('#tr-optional-fields').html(this.buildFieldRows(defs.optional, existingMappings));
        },

        buildFieldRows: function (fields, existingMappings) {
            if (!fields || !fields.length) {
                return '<p class="tr-text-muted">' + (tamrankSchemaBuilder.strings.no_fields_section || 'No fields in this section.') + '</p>';
            }

            var self = this;
            var html = '';

            fields.forEach(function (field) {
                var currentSource = '';
                if (existingMappings[field.key] && existingMappings[field.key].source) {
                    currentSource = existingMappings[field.key].source;
                } else if (field.sources.length > 0) {
                    currentSource = field.sources[0];
                }

                html += '<div class="tr-field-mapping-row">';
                html += '<div class="tr-field-mapping-label">';
                html += '<span>' + self.escHtml(field.label) + '</span>';
                if (field.required) {
                    html += ' <span class="tr-required">*</span>';
                }
                html += '</div>';
                html += '<div class="tr-field-mapping-control">';
                html += '<select class="tr-form-input tr-field-source-select" ';
                html += 'name="field_mappings[' + field.key + '][source]">';

                field.sources.forEach(function (src) {
                    var label = self.fieldSources[src] || src;
                    var selected = (src === currentSource) ? ' selected' : '';
                    html += '<option value="' + self.escHtml(src) + '"' + selected + '>';
                    html += self.escHtml(label) + '</option>';
                });

                html += '</select>';
                html += '</div>';
                html += '</div>';
            });

            return html;
        },

        /* ========================================
         * TEMPLATE SAVE (AJAX)
         * ======================================== */
        saveTemplate: function () {
            var self = this;
            var btn = $('#tr-save-template-btn');
            btn.prop('disabled', true).text(tamrankSchemaBuilder.strings.saving);

            var data = {
                action: 'tamrank_save_schema_template_ajax',
                nonce: this.nonce,
                name: $('#tr-tpl-name').val().trim(),
                schema_type: this.selectedSchemaType,
                post_types: [],
                field_mappings: {}
            };

            // Collect selected post types
            $('#tr-post-types-checkboxes input[type="checkbox"]:checked').each(function () {
                data.post_types.push($(this).val());
            });

            // Collect field mappings
            $('.tr-field-source-select').each(function () {
                var name = $(this).attr('name');
                var match = name.match(/field_mappings\[([^\]]+)\]\[source\]/);
                if (match) {
                    data.field_mappings[match[1]] = { source: $(this).val() };
                }
            });

            // Template ID for editing
            if (this.editingTemplateId) {
                data.template_id = this.editingTemplateId;
            }

            $.post(this.ajaxUrl, data, function (response) {
                btn.prop('disabled', false).html(tamrankSchemaBuilder.strings.template_save_btn + ' ' + SVG_CHECK);
                if (response.success) {
                    TamRankToast.success(
                        tamrankSchemaBuilder.strings.template_saved,
                        data.name + (tamrankSchemaBuilder.strings.template_saved_suffix || ' was saved successfully.')
                    );

                    // Update local cache
                    if (response.data && response.data.id) {
                        self.existingTemplates[response.data.id] = response.data.template;
                    }

                    // Reload page to refresh the templates table
                    window.location.reload();
                } else {
                    TamRankToast.show(tamrankSchemaBuilder.strings.error_save, response.data || (tamrankSchemaBuilder.strings.unknown_error || 'Unknown error'), 'error');
                }
            }).fail(function () {
                btn.prop('disabled', false).html(tamrankSchemaBuilder.strings.template_save_btn + ' ' + SVG_CHECK);
                TamRankToast.show(tamrankSchemaBuilder.strings.error_save, tamrankSchemaBuilder.strings.error_server, 'error');
            });
        },

        /* ========================================
         * TEMPLATE DELETE (AJAX)
         * ======================================== */
        deleteTemplate: function (id) {
            var self = this;
            var row = $('#tr-template-row-' + id);

            $.post(this.ajaxUrl, {
                action: 'tamrank_delete_schema_template_ajax',
                nonce: this.nonce,
                template_id: id
            }, function (response) {
                if (response.success) {
                    // Fade-out animation
                    row.addClass('tr-row-fade-out');
                    setTimeout(function () {
                        row.remove();
                        delete self.existingTemplates[id];

                        // Show empty message if no templates left
                        if ($('#tr-templates-tbody tr').length === 0) {
                            $('#tr-templates-tbody').html(
                                '<tr id="tr-templates-empty-row">' +
                                '<td colspan="5" class="tr-table-empty">' +
                                tamrankSchemaBuilder.strings.no_templates + '</td></tr>'
                            );
                        }
                    }, 400);

                    TamRankToast.success(tamrankSchemaBuilder.strings.template_deleted, tamrankSchemaBuilder.strings.template_deleted_desc);
                } else {
                    TamRankToast.show(tamrankSchemaBuilder.strings.error_delete, response.data || (tamrankSchemaBuilder.strings.unknown_error || 'Unknown error'), 'error');
                }
            }).fail(function () {
                TamRankToast.show(tamrankSchemaBuilder.strings.error_delete, tamrankSchemaBuilder.strings.error_server, 'error');
            });
        },

        /* ========================================
         * GLOBAL SETTINGS
         * ======================================== */
        bindGlobalSettings: function () {
            var self = this;

            // Entity type card selection
            $(document).on('click', '.tr-entity-card', function () {
                var entity = $(this).data('entity');

                // Update cards
                $('.tr-entity-card').removeClass('is-selected');
                $(this).addClass('is-selected');

                // Update hidden input
                $('#tr-entity-type-input').val(entity);

                // Show/hide sections based on entity type
                if (entity === 'Custom') {
                    $('#tr-entity-form').hide();
                    $('#tr-custom-jsonld-section').show();
                } else {
                    $('#tr-entity-form').show();
                    $('#tr-custom-jsonld-section').hide();
                }

                $('#tr-global-bench').toggleClass('is-lb', entity === 'LocalBusiness');
                if (entity === 'LocalBusiness') {
                    self.refreshPreview();
                }
                self.markDirty();
            });

            // Logo upload via WordPress media library
            $(document).on('click', '#tr-upload-logo-btn', function () {
                self.openMediaUploader();
            });

            $(document).on('click', '#tr-upload-social-fallback-btn', function () {
                self.openSocialFallbackUploader();
            });

            // JSON-LD validation
            $(document).on('click', '#tr-validate-jsonld-btn', function () {
                self.validateJsonld();
            });

            // Save global settings
            $(document).on('click', '#tr-save-global-btn', function () {
                self.saveGlobalSettings();
            });

            this.bindLocalBusiness();
            this.bindMerchant();
        },

        /* ========================================
         * MERCHANT LISTING (MERCH-01)
         * ======================================== */
        bindMerchant: function () {
            var self = this;
            if (!document.getElementById('tr-merch-card')) return;

            $(document).on('click', '#tr-merch-shipping-toggle', function () {
                var on = !$(this).hasClass('is-on');
                $(this).toggleClass('is-on', on).attr('aria-checked', on ? 'true' : 'false');
                $('#tr-merch-times, #tr-merch-shipping-note').toggle(on);
                self.markDirty();
            });

            $(document).on('click', '#tr-merch-return-toggle', function () {
                var on = !$(this).hasClass('is-on');
                $(this).toggleClass('is-on', on).attr('aria-checked', on ? 'true' : 'false');
                $('#tr-merch-return-fields').toggle(on);
                self.markDirty();
            });

            // The day count only exists for a finite window, and the fee amount
            // only for a fixed return fee — the same two rules the renderer
            // enforces before it ships anything.
            $(document).on('change', '#tr-merch-return-category', function () {
                $('#tr-merch-days-field').toggle($(this).val() === 'https://schema.org/MerchantReturnFiniteReturnWindow');
                self.markDirty();
            });

            $(document).on('change', '#tr-merch-return-fees', function () {
                $('#tr-merch-fee-field').toggle($(this).val() === 'https://schema.org/ReturnShippingFees');
                self.markDirty();
            });

            $(document).on('input change', '#tr-merch-card input, #tr-merch-card select', function () {
                self.markDirty();
            });
        },

        collectMerchantPayload: function (data) {
            if (!document.getElementById('tr-merch-card')) return data;

            // Every key sanitize_merchant() knows has to be posted here. The
            // handler stores the posted subtree whole, so a key left out is not
            // "unchanged" — it comes back as its empty default and wipes what
            // was saved before. The harness asserts this list against the
            // sanitizer's own keys so the two cannot drift apart again.
            var m = 'merchant';
            data[m + '[item_condition]']    = $('#tr-merch-condition').val() || '';
            data[m + '[shipping_enabled]']  = $('#tr-merch-shipping-toggle').hasClass('is-on') ? '1' : '';
            data[m + '[handling_time_min]'] = $('#tr-merch-handling-min').val() || '';
            data[m + '[handling_time_max]'] = $('#tr-merch-handling-max').val() || '';
            data[m + '[transit_time_min]']  = $('#tr-merch-transit-min').val() || '';
            data[m + '[transit_time_max]']  = $('#tr-merch-transit-max').val() || '';
            data[m + '[return_enabled]']    = $('#tr-merch-return-toggle').hasClass('is-on') ? '1' : '';
            data[m + '[return_country]']    = $('#tr-merch-return-country').val() || '';
            data[m + '[return_category]']   = $('#tr-merch-return-category').val() || '';
            data[m + '[return_days]']       = $('#tr-merch-return-days').val() || '';
            data[m + '[return_method]']     = $('#tr-merch-return-method').val() || '';
            data[m + '[return_fees]']       = $('#tr-merch-return-fees').val() || '';
            data[m + '[return_fee_amount]'] = $('#tr-merch-return-fee-amount').val() || '';

            return data;
        },

        /* ========================================
         * LOCALBUSINESS (variant C workbench)
         * ======================================== */
        lbDirty: false,
        lbPreviewTimer: null,
        lbExpanded: {},
        lbShowAll: false,
        lbLastNode: null,
        lbGhosts: {},
        lbGhostTimer: null,

        bindLocalBusiness: function () {
            var self = this;
            if (!document.getElementById('tr-global-bench')) return;

            // Accordions (sections 3, 4, 5)
            $(document).on('click', '[data-lb-toggle]', function () {
                $('#' + $(this).data('lb-toggle')).toggleClass('is-open');
            });

            // Business subtype drives the conditional food fields
            $(document).on('change', '#tr-lb-subtype', function () {
                var food = ['Restaurant', 'FoodEstablishment'].indexOf($(this).val()) !== -1;
                $('#tr-lb-food-fields').toggle(food);
            });

            // Address toggle
            $(document).on('click', '#tr-lb-visit-toggle', function () {
                var on = !$(this).hasClass('is-on');
                self.setVisitToggle(on);
                self.markDirty();
                self.refreshPreview();
            });

            // Opening hours: the closed checkbox greys out that row
            $(document).on('change', '#tr-lb-hours .tr-lb-cb-closed', function () {
                $(this).closest('.tr-lb-hrow').toggleClass('is-closed', this.checked);
            });

            $(document).on('click', '#tr-lb-copy-mon-week', function () { self.copyMonday(false); });
            $(document).on('click', '#tr-lb-copy-mon-all', function () { self.copyMonday(true); });

            // De terugkoppeling gaat over de vorige klik; zodra de gebruiker de
            // uren aanpast klopt hij niet meer en verdwijnt hij.
            $(document).on('input change', '#tr-lb-hours input', function () { $('#tr-lb-copy-msg').hide(); });

            // Online profiles
            $(document).on('click', '#tr-lb-add-profile', function () {
                $('#tr-lb-profiles').append(
                    '<div class="tr-lb-profile-row">' +
                    '<input type="url" class="tr-form-input tr-lb-profile" placeholder="https://…">' +
                    '<button type="button" class="tr-lb-icon-btn tr-lb-profile-remove" aria-label="' +
                    self.escHtml(tamrankSchemaBuilder.strings.delete || 'Delete') + '">×</button></div>'
                );
                $('#tr-lb-profiles .tr-lb-profile').last().trigger('focus');
                self.markDirty();
            });
            $(document).on('click', '.tr-lb-profile-remove', function () {
                $(this).closest('.tr-lb-profile-row').remove();
                self.markDirty();
                self.refreshPreview();
            });

            // Output location
            $(document).on('change', 'input[name="tr_lb_output"]', function () {
                $('.tr-lb-radio').each(function () {
                    $(this).toggleClass('is-on', $(this).find('input').prop('checked'));
                });
                $('#tr-lb-contact-pick').toggle($(this).val() === 'homepage_contact');
                self.markDirty();
            });

            // Extra images via the media library — stored as attachment IDs
            $(document).on('click', '#tr-lb-pick-images', function () { self.openImagePicker(); });
            $(document).on('click', '#tr-lb-clear-images', function () {
                $('#tr-lb-images').val('');
                $('#tr-lb-images-label').val('');
                self.markDirty();
                self.refreshPreview();
            });

            // JSON panel: click a line to unfold it, or unfold everything
            $(document).on('click', '#tr-lb-json .tr-lb-jline.is-exp', function () {
                var key = $(this).data('k');
                if (self.lbExpanded[key]) { delete self.lbExpanded[key]; } else { self.lbExpanded[key] = true; }
                self.paintJson(self.lbLastNode, false);
            });
            $(document).on('click', '#tr-lb-json-all', function () {
                self.lbShowAll = !self.lbShowAll;
                $(this).text(self.lbShowAll
                    ? (tamrankSchemaBuilder.strings.lb_collapse || 'Collapse')
                    : (tamrankSchemaBuilder.strings.lb_show_all || 'Show all'));
                self.paintJson(self.lbLastNode, false);
            });

            // Anything the user types in the form is a change worth previewing
            $(document).on('input change', '#tr-global-bench .tr-lb-formcol input, #tr-global-bench .tr-lb-formcol select, #tr-global-bench .tr-lb-formcol textarea', function () {
                self.markDirty();
                self.schedulePreview();
            });

            if ($('#tr-global-bench').hasClass('is-lb')) {
                this.refreshPreview();
            }
        },

        setVisitToggle: function (on) {
            $('#tr-lb-visit-toggle').toggleClass('is-on', on).attr('aria-checked', on ? 'true' : 'false');
            $('#tr-lb-address-fields').toggleClass('tr-lb-dim', !on);
            $('#tr-lb-visit-off-note').toggle(!on);
            $('.tr-lb-addr-req').toggle(on);
            $('.tr-lb-area-req').toggle(!on);
            $('#tr-lb-visit-sub').text(on
                ? (tamrankSchemaBuilder.strings.lb_visit_sub_on || '')
                : (tamrankSchemaBuilder.strings.lb_visit_sub_off || ''));
        },

        /**
         * Kopieer maandag naar de rest van de week.
         *
         * Een <input type="time"> geeft een LEGE string terug zodra ook maar één
         * segment ontbreekt (validity.badInput) — je typt "17", ziet "17:--" staan,
         * en .val() is ''. De oude versie schreef die lege waarde door naar de
         * andere dagen en wiste daarmee stil wat daar stond. Vandaar: alleen
         * kopiëren wat compleet is, en anders niets aanraken plus uitleggen waarom.
         */
        copyMonday: function (allDays) {
            var rows = $('#tr-lb-hours .tr-lb-hrow');
            var src = rows.eq(0);
            var $opens = src.find('.tr-lb-opens');
            var $closes = src.find('.tr-lb-closes');
            var s = tamrankSchemaBuilder.strings;

            if (src.find('.tr-lb-cb-closed').prop('checked')) {
                this.copyMsg(false, s.lb_copy_msg_closed);
                return;
            }
            if (($opens[0] && $opens[0].validity.badInput) || ($closes[0] && $closes[0].validity.badInput)) {
                this.copyMsg(false, s.lb_copy_msg_incomplete);
                return;
            }

            var opens = $opens.val();
            var closes = $closes.val();
            if (!opens || !closes) {
                this.copyMsg(false, s.lb_copy_msg_empty);
                return;
            }

            rows.slice(1, allDays ? 7 : 5).each(function () {
                $(this).find('.tr-lb-opens').val(opens);
                $(this).find('.tr-lb-closes').val(closes);
                $(this).find('.tr-lb-cb-closed').prop('checked', false);
                $(this).removeClass('is-closed');
            });
            this.copyMsg(true, fillPlaceholder(allDays ? s.lb_copy_msg_done_all : s.lb_copy_msg_done_week,
                '%s', opens + '–' + closes));
            this.markDirty();
            this.refreshPreview();
        },

        /** Terugkoppeling onder de kopieerknoppen; stil falen is geen optie. */
        copyMsg: function (ok, text) {
            var $el = $('#tr-lb-copy-msg');
            if (!$el.length || !text) return;
            $el.toggleClass('tr-lb-note--ok', !!ok).toggleClass('tr-lb-note--warn', !ok);
            $('#tr-lb-copy-msg-icon').text(ok ? '✓' : '⚠');
            $('#tr-lb-copy-msg-text').text(text);
            $el.show();
        },

        openImagePicker: function () {
            var self = this;
            var frame = wp.media({
                title: tamrankSchemaBuilder.strings.lb_pick_media || 'Choose media',
                button: { text: tamrankSchemaBuilder.strings.lb_pick_media || 'Choose media' },
                library: { type: 'image' },
                multiple: true
            });
            frame.on('select', function () {
                var ids = [], names = [];
                frame.state().get('selection').each(function (item) {
                    var a = item.toJSON();
                    ids.push(a.id);
                    names.push(a.filename || a.title || a.id);
                });
                $('#tr-lb-images').val(ids.join(','));
                $('#tr-lb-images-label').val(names.join(', '));
                self.markDirty();
                self.refreshPreview();
            });
            frame.open();
        },

        /* --- dirty state: the Rich Results test reads the site, not this form --- */
        markDirty: function () {
            if (this.lbDirty) return;
            this.lbDirty = true;
            $('#tr-lb-unsaved').show();
            $('#tr-lb-test-dirty').show();
        },

        clearDirty: function () {
            this.lbDirty = false;
            $('#tr-lb-unsaved').hide();
            $('#tr-lb-test-dirty').hide();
        },

        /* --- payload: one collector, used by both save and preview --- */
        collectLbPayload: function () {
            var data = {
                entity_type: $('#tr-entity-type-input').val(),
                organization_name: $('input[name="tr_org_name"]').val(),
                website_url: $('input[name="tr_org_url"]').val(),
                email: $('input[name="tr_org_email"]').val(),
                telephone: $('input[name="tr_org_telephone"]').val(),
                logo_url: $('#tr-logo-url').val(),
                default_social_image: $('#tr-social-fallback-url').val() || '',
                default_social_image_id: $('#tr-social-fallback-id').val() || 0,
                'address[street]': $('input[name="tr_address_street"]').val(),
                'address[postal_code]': $('input[name="tr_address_postal"]').val(),
                'address[city]': $('input[name="tr_address_city"]').val(),
                'address[country]': $('select[name="tr_address_country"]').val()
            };

            var lb = 'localbusiness';
            var loc = lb + '[locations][0]';

            data[lb + '[output_location]'] = $('input[name="tr_lb_output"]:checked').val() || 'homepage';
            data[lb + '[contact_page_id]'] = $('#tr-lb-contact-page').val() || 0;

            data[loc + '[subtype]'] = $('#tr-lb-subtype').val() || 'LocalBusiness';
            data[loc + '[customers_visit]'] = $('#tr-lb-visit-toggle').hasClass('is-on') ? '1' : '';
            data[loc + '[area_served]'] = $('#tr-lb-area').val() || '';
            data[loc + '[geo][lat]'] = $('#tr-lb-lat').val() || '';
            data[loc + '[geo][lng]'] = $('#tr-lb-lng').val() || '';
            data[loc + '[price_range]'] = $('#tr-lb-price').val() || '';
            data[loc + '[founding_date]'] = $('#tr-lb-founding').val() || '';
            data[loc + '[vat_id]'] = $('#tr-lb-vat').val() || '';
            data[loc + '[tax_id]'] = $('#tr-lb-tax').val() || '';
            data[loc + '[contact_point][telephone]'] = $('#tr-lb-cp-tel').val() || '';
            data[loc + '[contact_point][contact_type]'] = $('#tr-lb-cp-type').val() || '';
            data[loc + '[payment_accepted]'] = $('#tr-lb-payment').val() || '';
            data[loc + '[serves_cuisine]'] = $('#tr-lb-cuisine').val() || '';
            data[loc + '[accepts_reservations]'] = $('#tr-lb-reservations').val() || '';

            var imageIds = ($('#tr-lb-images').val() || '').split(',').filter(function (id) { return id !== ''; });
            if (imageIds.length) {
                imageIds.forEach(function (id, i) { data[loc + '[images][' + i + ']'] = id; });
            } else {
                data[loc + '[images]'] = '';
            }

            $('#tr-lb-hours .tr-lb-hrow').each(function () {
                var day = $(this).data('day');
                data[loc + '[opening_hours][' + day + '][closed]'] = $(this).find('.tr-lb-cb-closed').prop('checked') ? '1' : '';
                data[loc + '[opening_hours][' + day + '][opens]'] = $(this).find('.tr-lb-opens').val() || '';
                data[loc + '[opening_hours][' + day + '][closes]'] = $(this).find('.tr-lb-closes').val() || '';
            });

            var profiles = [];
            $('#tr-lb-profiles .tr-lb-profile').each(function () {
                var value = $.trim($(this).val());
                if (value) profiles.push(value);
            });
            if (profiles.length) {
                profiles.forEach(function (url, i) { data['social_profiles[' + i + ']'] = url; });
            } else {
                data['social_profiles'] = '';
            }

            // The merchant card rides along on the same save. It only exists
            // when WooCommerce is active, so on a site without a shop this is a
            // no-op and the subtree keeps whatever was stored.
            data = this.collectMerchantPayload(data);

            return data;
        },

        /* --- live preview: the PHP builder is the single source of truth --- */
        schedulePreview: function () {
            var self = this;
            if (!$('#tr-global-bench').hasClass('is-lb')) return;
            clearTimeout(this.lbPreviewTimer);
            this.lbPreviewTimer = setTimeout(function () { self.refreshPreview(); }, 350);
        },

        refreshPreview: function () {
            var self = this;
            if (!document.getElementById('tr-lb-json')) return;

            var data = this.collectLbPayload();
            data.action = 'tamrank_preview_localbusiness_schema';
            data.nonce = this.nonce;

            $.post(this.ajaxUrl, data, function (response) {
                if (!response || !response.success || !response.data) return;
                self.paintJson(response.data.node, true);
                self.paintCoverage(response.data.node);
                self.paintSummaries(response.data.node);
            });
        },

        /* --- panel painting: presentation only, on the JSON the server returned --- */
        paintJson: function (node, isNew) {
            if (!node) return;
            var self = this;
            var out = ['{'];
            var keys = Object.keys(node);
            var previous = this.lbLastNode;
            var now = Date.now();

            keys.forEach(function (key, i) {
                var value = node[key];
                var comma = i < keys.length - 1 ? ',' : '';
                var changed = isNew && previous && JSON.stringify(previous[key]) !== JSON.stringify(value);
                var cls = changed ? ' is-chg' : '';

                if (value !== null && typeof value === 'object') {
                    var isArr = Array.isArray(value);
                    var size = isArr
                        ? value.length + ' ' + (value.length === 1
                            ? (tamrankSchemaBuilder.strings.lb_item || 'item')
                            : (tamrankSchemaBuilder.strings.lb_items || 'items'))
                        : (Object.keys(value).length - ('@type' in value ? 1 : 0)) + ' ' + (tamrankSchemaBuilder.strings.lb_fields || 'fields');

                    if (self.lbShowAll || self.lbExpanded[key]) {
                        var body = JSON.stringify(value, null, 2).split('\n').map(function (line, j) {
                            return j === 0 ? line : '  ' + line;
                        }).join('\n');
                        out.push('<span class="tr-lb-jline is-exp' + cls + '" data-k="' + self.escHtml(key) + '">  "<span class="tr-lb-k">' +
                            self.escHtml(key) + '</span>": ' + self.colorJson(body) + comma + '</span>');
                    } else {
                        out.push('<span class="tr-lb-jline is-exp' + cls + '" data-k="' + self.escHtml(key) + '">  "<span class="tr-lb-k">' +
                            self.escHtml(key) + '</span>": ' + (isArr ? '[' : '{') + ' <span class="tr-lb-dots">… ' + self.escHtml(size) +
                            ' — ' + self.escHtml(tamrankSchemaBuilder.strings.lb_click_open || 'click to open') + '</span> ' +
                            (isArr ? ']' : '}') + comma + '</span>');
                    }
                } else {
                    out.push('<span class="tr-lb-jline' + cls + '">  ' +
                        self.colorJson('"' + key + '": ' + JSON.stringify(value)) + comma + '</span>');
                }
            });

            // A key that dropped out of the output fades as a ghost line instead
            // of vanishing without a trace.
            if (isNew && previous) {
                Object.keys(previous).forEach(function (key) {
                    if (!(key in node)) self.lbGhosts[key] = now;
                });
            }
            Object.keys(this.lbGhosts).forEach(function (key) {
                if (key in node || now - self.lbGhosts[key] > 1500) { delete self.lbGhosts[key]; return; }
                out.push('<span class="tr-lb-jline is-ghost">  <s>"' + self.escHtml(key) + '"</s> — ' +
                    self.escHtml(tamrankSchemaBuilder.strings.lb_dropped || 'just dropped out of the output') + '</span>');
            });
            out.push('}');

            document.getElementById('tr-lb-json').innerHTML = out.join('\n');
            this.lbLastNode = node;

            if (Object.keys(this.lbGhosts).length && !this.lbGhostTimer) {
                this.lbGhostTimer = setTimeout(function () {
                    self.lbGhostTimer = null;
                    self.paintJson(self.lbLastNode, false);
                }, 1600);
            }
        },

        colorJson: function (text) {
            return this.escHtml(text)
                .replace(/"([^"]+)":/g, '"<span class="tr-lb-k">$1</span>":')
                .replace(/: "([^"]*)"/g, ': "<span class="tr-lb-s">$1</span>"')
                .replace(/: (-?\d+\.?\d*)/g, ': <span class="tr-lb-n">$1</span>')
                .replace(/^(\s*)"([^"]+)"(,?)$/gm, '$1"<span class="tr-lb-s">$2</span>"$3');
        },

        paintCoverage: function (node) {
            var self = this;
            var visits = $('#tr-lb-visit-toggle').hasClass('is-on');
            var s = tamrankSchemaBuilder.strings;

            var required = [
                { t: s.lb_cov_name || 'name', ok: !!node.name },
                visits
                    ? { t: s.lb_cov_address || 'address', ok: !!node.address }
                    : { t: s.lb_cov_area || 'areaServed', ok: !!node.areaServed, critical: true }
            ];
            var recommended = [
                { t: 'telephone', ok: !!node.telephone },
                { t: 'logo', ok: !!node.logo },
                { t: 'openingHoursSpecification', ok: !!node.openingHoursSpecification },
                { t: 'sameAs', ok: !!node.sameAs },
                { t: 'geo', ok: !!node.geo },
                { t: 'priceRange', ok: !!node.priceRange },
                { t: 'image', ok: !!node.image }
            ];

            var row = function (o) {
                var state = o.ok ? 'is-ok' : (o.critical ? 'is-bad' : 'is-miss');
                var icon = o.ok ? '✓' : (o.critical ? '!' : '○');
                return '<div class="tr-lb-chk-row ' + state + '"><span class="tr-lb-chk-ic">' + icon +
                    '</span><span>' + self.escHtml(o.t) + '</span></div>';
            };

            document.getElementById('tr-lb-chk-req').innerHTML = required.map(row).join('');
            document.getElementById('tr-lb-chk-rec').innerHTML = recommended.map(row).join('');

            var done = required.concat(recommended).filter(function (o) { return o.ok; }).length;
            var total = required.length + recommended.length;
            $('#tr-lb-cov-txt').text(done + '/' + total);
            $('#tr-lb-cov-bar').css('width', Math.round(done / total * 100) + '%');
        },

        paintSummaries: function (node) {
            var s = tamrankSchemaBuilder.strings;
            var empty = '<span class="tr-lb-sum-empty">' + this.escHtml(s.lb_nothing_yet || 'Nothing filled in yet') + '</span>';

            var hours = node.openingHoursSpecification;
            if (hours && hours.length) {
                var first = hours[0];
                var last = hours[hours.length - 1];
                var from = first.dayOfWeek[0].slice(0, 3);
                var to = last.dayOfWeek[last.dayOfWeek.length - 1].slice(0, 3);
                $('#tr-lb-sum3').html('<b>' + this.escHtml(from + '–' + to + ' ' + first.opens + '–' + first.closes) + '</b>');
            } else {
                $('#tr-lb-sum3').html(empty);
            }

            var count = node.sameAs ? node.sameAs.length : 0;
            $('#tr-lb-sum4').html(count
                ? '<b>' + this.escHtml(count + ' ' + (count === 1
                    ? (s.lb_profile_single || 'profile')
                    : (s.lb_profile_plural || 'profiles')) + ' ' + (s.lb_linked || 'linked')) + '</b>'
                : empty);
        },

        openMediaUploader: function () {
            var frame = wp.media({
                title: tamrankSchemaBuilder.strings.choose_logo,
                button: { text: tamrankSchemaBuilder.strings.use_as_logo },
                multiple: false
            });

            frame.on('select', function () {
                var attachment = frame.state().get('selection').first().toJSON();
                $('#tr-logo-url').val(attachment.url);
            });

            frame.open();
        },

        /* The site-wide fallback share image. Its own picker rather than a
           shared one: the modal copy names what you are choosing, and the
           attachment ID is stored alongside the URL so the picker reopens on
           the right item. */
        openSocialFallbackUploader: function () {
            var frame = wp.media({
                title: tamrankSchemaBuilder.strings.choose_social_image,
                button: { text: tamrankSchemaBuilder.strings.use_as_social_image },
                multiple: false
            });

            frame.on('select', function () {
                var attachment = frame.state().get('selection').first().toJSON();
                $('#tr-social-fallback-url').val(attachment.url);
                $('#tr-social-fallback-id').val(attachment.id || 0);
            });

            frame.open();
        },

        validateJsonld: function () {
            var code = $('#tr-custom-jsonld-editor').val().trim();
            if (!code) {
                TamRankToast.show(tamrankSchemaBuilder.strings.fill_jsonld, '', 'warning');
                return;
            }

            var btn = $('#tr-validate-jsonld-btn');
            btn.prop('disabled', true).text(tamrankSchemaBuilder.strings.validating);

            $.post(this.ajaxUrl, {
                action: 'tamrank_validate_jsonld',
                nonce: this.nonce,
                json_code: code
            }, function (response) {
                btn.prop('disabled', false).html(SVG_CHECK + ' ' + (tamrankSchemaBuilder.strings.validate_jsonld_btn || 'Validate JSON-LD'));

                if (response.success && response.data) {
                    var result = response.data;
                    if (result.valid && result.errors.length === 0) {
                        var msg = (tamrankSchemaBuilder.strings.jsonld_valid || 'JSON-LD is valid.');
                        if (result.warnings.length > 0) {
                            msg += ' ' + result.warnings.length + ' ' + (tamrankSchemaBuilder.strings.warnings_suffix || 'warning(s).');
                        }
                        TamRankToast.success(tamrankSchemaBuilder.strings.validation_success, msg);
                    } else {
                        var errorMsg = result.errors.join('. ');
                        if (result.warnings.length > 0) {
                            errorMsg += ' ' + (tamrankSchemaBuilder.strings.warnings_label || 'Warnings:') + ' ' + result.warnings.join('. ');
                        }
                        TamRankToast.show(tamrankSchemaBuilder.strings.validation_failed, errorMsg, 'error');
                    }
                } else {
                    TamRankToast.show(tamrankSchemaBuilder.strings.validation_error, response.data || (tamrankSchemaBuilder.strings.unknown_error || 'Unknown error'), 'error');
                }
            }).fail(function () {
                btn.prop('disabled', false).html(SVG_CHECK + ' ' + (tamrankSchemaBuilder.strings.validate_jsonld_btn || 'Validate JSON-LD'));
                TamRankToast.show(tamrankSchemaBuilder.strings.validation_error, tamrankSchemaBuilder.strings.error_server, 'error');
            });
        },

        saveGlobalSettings: function () {
            var self = this;
            var btn = $('#tr-save-global-btn');
            btn.prop('disabled', true).text(tamrankSchemaBuilder.strings.saving);

            // Same collector as the live preview: the panel and the save can
            // never disagree about what the form currently says. The complete
            // localbusiness subtree goes along every time — update_settings()
            // merges shallowly, so a partial subtree would drop keys.
            var data = this.collectLbPayload();
            data.action = 'tamrank_save_global_schema_ajax';
            data.nonce = this.nonce;
            data.custom_jsonld_code = $('#tr-custom-jsonld-editor').val();

            $.post(this.ajaxUrl, data, function (response) {
                btn.prop('disabled', false).html(tamrankSchemaBuilder.strings.settings_save_btn + ' ' + SVG_CHECK);
                if (response.success) {
                    self.clearDirty();
                    TamRankToast.success(
                        tamrankSchemaBuilder.strings.settings_saved,
                        tamrankSchemaBuilder.strings.settings_saved_desc
                    );
                } else {
                    TamRankToast.show(tamrankSchemaBuilder.strings.error_save_settings, response.data || (tamrankSchemaBuilder.strings.unknown_error || 'Unknown error'), 'error');
                }
            }).fail(function () {
                btn.prop('disabled', false).html(tamrankSchemaBuilder.strings.settings_save_btn + ' ' + SVG_CHECK);
                TamRankToast.show(tamrankSchemaBuilder.strings.error_save_settings, tamrankSchemaBuilder.strings.error_server, 'error');
            });
        },

        /* ========================================
         * UTILITY
         * ======================================== */
        escHtml: function (str) {
            if (!str) return '';
            var div = document.createElement('div');
            div.appendChild(document.createTextNode(str));
            return div.innerHTML;
        }
    };

    // Initialize on DOM ready
    $(document).ready(function () {
        SchemaBuilder.init();
    });

})(jQuery);
