<?php

if (!defined('ABSPATH')) { exit; }

class AIEO_DMM_Product_Display extends AIEO_DMM_Module_Base {

    
    const ORDER_OPTION = 'aieo_dmm_product_display_order';

    
    const RECO_ORDER_OPTION = 'aieo_dmm_recommendations_order';

    
    const TAB_ORDER_OPTION = 'aieo_dmm_pdp_tab_order';

    
    public static function builtin_reco_surfaces() {
        return array( 'same_needs', 'crosssells', 'upsells', 'related', 'recently_viewed' );
    }

    
    public static function builtin_reco_legacy_visibility_keys() {
        return array(
            'same_needs'      => 'aieo_display_same_needs',
            'crosssells'      => 'aieo_display_cross_sells',
            'upsells'         => 'aieo_display_upsells',
            'related'         => 'aieo_display_related',
            'recently_viewed' => 'aieo_display_recently_viewed_products',
        );
    }

    
    public static function orderable_cluster_ids() {
        
        
        
        
        
        
        return array( 'role_pricing_login', 'product_meta', 'product_categories', 'brand_display', 'payment_logos', 'trust_badges', 'product_needs', 'complementary', 'waitlist', 'brand_gifts', 'wishlist' );
    }

    public function get_id()       { return 'product_display'; }
    public function get_label()    { return __('Dashboard', 'ai-eshop-optimizer'); }
    public function get_priority() { return 5; } 
    public function get_group()    { return 'product_display'; }

    public function register_hooks() {
        add_action('wp_ajax_aieo_dmm_product_display_toggle',     array($this, 'ajax_toggle'));
        add_action('wp_ajax_aieo_dmm_product_display_save_order', array($this, 'ajax_save_order'));
        
        
        
        
        
        add_action('wp_ajax_aieo_dmm_recommendations_save_order', array($this, 'ajax_save_reco_order'));
        
        
        
        
        
        
        add_action('wp_ajax_aieo_dmm_save_tab_order', array($this, 'ajax_save_tab_order'));
        add_filter('woocommerce_product_tabs', array($this, 'apply_tab_order'), 98);
        
        
        add_action('wp_enqueue_scripts', array($this, 'enqueue_cluster_order_css'));
        add_action('wp_enqueue_scripts', array($this, 'enqueue_recommendations_order_css'));
        
        
        add_action('wp_enqueue_scripts', array($this, 'enqueue_gallery_css'));
        
        
        
        add_filter('woocommerce_gallery_image_html_attachment_image_params', array($this, 'gallery_image_sizes'), 20, 4);
        
        
        
        
        
        
        add_action('wp', array($this, 'reorder_classic_theme_recos'), 100);
        
        
        
        
        
        
        
        
        
        
        
        if ( ! defined( 'AIEO_RUNTIME_OWNS_AJAX' ) ) {
            add_action('wp', array($this, 'reorder_classic_theme_summary'), 100);
        }
        
        
        
        
        
        add_filter('render_block', array($this, 'wrap_cluster_block_in_template'), 10, 2);
    }

    
    private function modules_catalog() {
        return array(
            array(
                'id'          => 'payment_logos',
                'label'       => __('Payment Logos', 'ai-eshop-optimizer'),
                'description' => __('Show payment-method icons (Visa, Mastercard, COD, etc.) under the Add-to-Cart button.', 'ai-eshop-optimizer'),
                'option_key'  => 'aieo_dmm_payment_logos_settings',
                'icon'        => 'dashicons-money-alt',
            ),
            array(
                'id'          => 'product_labels',
                'label'       => __('Product Labels', 'ai-eshop-optimizer'),
                'description' => __('"Sale", "New", "Best seller" badges over product images on shop + single pages.', 'ai-eshop-optimizer'),
                'option_key'  => 'aieo_dmm_product_labels_settings',
                'icon'        => 'dashicons-tag',
            ),
            array(
                'id'          => 'add_to_cart_text',
                'label'       => __('Add to Cart Text', 'ai-eshop-optimizer'),
                'description' => __('Customise "Add to cart" / "Select options" / "Out of stock" labels per product type.', 'ai-eshop-optimizer'),
                'option_key'  => 'aieo_dmm_add_to_cart_text_settings',
                'icon'        => 'dashicons-cart',
            ),
            array(
                'id'          => 'trust_badges',
                'label'       => __('Trust Badges', 'ai-eshop-optimizer'),
                'description' => __('Reassurance badges (free shipping, easy returns, secure checkout) on product pages.', 'ai-eshop-optimizer'),
                'option_key'  => 'aieo_dmm_trust_badges_settings',
                'icon'        => 'dashicons-shield',
            ),
            array(
                'id'          => 'product_meta',
                'label'       => __('Product Meta', 'ai-eshop-optimizer'),
                'description' => __('Custom display of SKU, categories, brand, tags and other product meta on the single page.', 'ai-eshop-optimizer'),
                'option_key'  => 'aieo_dmm_product_meta_settings',
                'icon'        => 'dashicons-info',
            ),
            array(
                'id'          => 'product_categories',
                'label'       => __('Product Categories', 'ai-eshop-optimizer'),
                'description' => __('Clickable, comma-separated list of the product’s categories (each links to its archive). Restores the line WooCommerce’s native meta used to show.', 'ai-eshop-optimizer'),
                'option_key'  => 'aieo_dmm_product_categories_settings',
                'icon'        => 'dashicons-category',
            ),
            array(
                'id'          => 'role_pricing_login',
                'label'       => __('Role-Pricing Login Message', 'ai-eshop-optimizer'),
                'description' => __('The “log in to see your price” message for role-based pricing, shown in the info column. Enable it from the Role Pricing tab (tick “Position in the product info column”).', 'ai-eshop-optimizer'),
                'option_key'  => 'aieo_dmm_role_pricing_login_settings',
                'icon'        => 'dashicons-lock',
            ),
            array(
                'id'          => 'brand_display',
                'label'       => __('Brand Display', 'ai-eshop-optimizer'),
                'description' => __('Show the product brand on single product pages — as text or as the brand logo image. Position is configurable (before/in/after the title row, or after the post-meta).', 'ai-eshop-optimizer'),
                'option_key'  => 'aieo_dmm_brand_display_settings',
                'icon'        => 'dashicons-awards',
            ),
            array(
                'id'          => 'product_video',
                'label'       => __('Product Video', 'ai-eshop-optimizer'),
                'description' => __('Embed a video (YouTube / Vimeo / self-hosted) into the product gallery.', 'ai-eshop-optimizer'),
                'option_key'  => 'aieo_dmm_product_video_settings',
                'icon'        => 'dashicons-video-alt3',
            ),
            array(
                'id'          => 'variations_gallery',
                'label'       => __('Variations Gallery', 'ai-eshop-optimizer'),
                'description' => __('Per-variation image galleries — switching attributes swaps the gallery for that variation.', 'ai-eshop-optimizer'),
                'option_key'  => 'aieo_dmm_variations_gallery_settings',
                'icon'        => 'dashicons-images-alt2',
            ),
            array(
                'id'          => 'product_needs',
                'label'       => __('Product Needs', 'ai-eshop-optimizer'),
                'description' => __('"This product covers:" badge listing the linked pa_need terms. Renders inside the after-cart cluster on FSE templates.', 'ai-eshop-optimizer'),
                'option_key'  => 'aieo_dmm_product_needs_display',
                'icon'        => 'dashicons-yes-alt',
            ),
            array(
                'id'          => 'complementary',
                'label'       => __('Complementary Products', 'ai-eshop-optimizer'),
                'description' => __('"Complete your purchase with…" picker. Renders inside the after-cart cluster on FSE templates (was hooked into the WC add-to-cart form on classic themes).', 'ai-eshop-optimizer'),
                'option_key'  => 'aieo_dmm_complementary_settings',
                'icon'        => 'dashicons-plus-alt',
            ),
            array(
                'id'          => 'waitlist',
                'label'       => __('Back-in-Stock Waitlist', 'ai-eshop-optimizer'),
                'description' => __('Email-subscribe form shown on out-of-stock products. Renders inside the cluster on FSE templates (was hooked into the WC add-to-cart form on classic themes).', 'ai-eshop-optimizer'),
                'option_key'  => 'aieo_dmm_waitlist',
                'icon'        => 'dashicons-email-alt',
            ),
            array(
                'id'          => 'brand_gifts',
                'label'       => __('Free Brand Gifts', 'ai-eshop-optimizer'),
                'description' => __('"Buy ≥ €X and get…" gift offers from the product\'s brand. Renders inside the cluster on FSE templates (was hooked into the WC product summary on classic themes).', 'ai-eshop-optimizer'),
                'option_key'  => 'aieo_dmm_brand_gifts_settings',
                'icon'        => 'dashicons-products',
            ),
        );
    }

    public static function module_is_enabled($option_key) {
        $opt = get_option($option_key, array());
        if (!is_array($opt)) { $opt = array(); }
        
        return !empty($opt['enabled']);
    }

    public function ajax_toggle() {
        AIEO_DMM::verify_ajax_request();
        $module = isset($_POST['module']) ? sanitize_key(wp_unslash($_POST['module'])) : '';
        $enabled = !empty($_POST['enabled']);
        $catalog = $this->modules_catalog();
        foreach ($catalog as $m) {
            if ($m['id'] !== $module) { continue; }
            $stored = get_option($m['option_key'], array());
            if (!is_array($stored)) { $stored = array(); }
            $stored['enabled'] = $enabled;
            update_option($m['option_key'], $stored, false);
            wp_send_json_success(array(
                'enabled' => $enabled,
                'message' => $enabled
                    ? __('Module enabled.', 'ai-eshop-optimizer')
                    : __('Module disabled.', 'ai-eshop-optimizer'),
            ));
            return;
        }
        wp_send_json_error(array('message' => __('Unknown module.', 'ai-eshop-optimizer')));
    }

    
    public static function get_cluster_order() {
        $defaults = self::orderable_cluster_ids();
        $stored   = get_option(self::ORDER_OPTION, array());
        if (!is_array($stored) || empty($stored)) { return $defaults; }
        
        $known = array_values(array_intersect($stored, $defaults));
        
        foreach ($defaults as $id) {
            if (!in_array($id, $known, true)) { $known[] = $id; }
        }
        return $known;
    }

    
    const ZONES_OPTION = 'aieo_dmm_product_display_zones';

    
    const LAYOUT_ORDER_OPTION = 'aieo_dmm_pdp_layout_order';

    
    public static function layout_catalog() {
        return array(
            
            'title'              => array('label' => __('Product Title', 'ai-eshop-optimizer'),            'sel' => '.ff-order--title',        'zoneable' => false),
            'rating'             => array('label' => __('Product Rating', 'ai-eshop-optimizer'),           'sel' => '.ff-order--rating',       'zoneable' => false),
            'summary'            => array('label' => __('Short Description', 'ai-eshop-optimizer'),         'sel' => '.ff-order--summary',      'zoneable' => false),
            'sale_msg'           => array('label' => __('Sale-eligibility message', 'ai-eshop-optimizer'), 'sel' => '.ff-order--sale_msg',     'zoneable' => false),
            'free_shipping'      => array('label' => __('Free-shipping message', 'ai-eshop-optimizer'),    'sel' => '.ff-order--free_shipping','zoneable' => false),
            'lowest_price'       => array('label' => __('Lowest-price (sale) message', 'ai-eshop-optimizer'), 'sel' => '.ff-order--lowest_price', 'zoneable' => false),
            'price'              => array('label' => __('Price', 'ai-eshop-optimizer'),                    'sel' => '.ff-order--price',        'zoneable' => false),
            'add_to_cart'        => array('label' => __('Add to Cart', 'ai-eshop-optimizer'),              'sel' => '.ff-order--add_to_cart',  'zoneable' => false),
            
            'product_meta'       => array('label' => __('Product Meta (SKU / barcode)', 'ai-eshop-optimizer'), 'sel' => '.wp-block-aieo-post-meta',          'zoneable' => true),
            'product_categories' => array('label' => __('Product Categories', 'ai-eshop-optimizer'),       'sel' => '.aieo-dmm-bridge--product_categories', 'zoneable' => true),
            'role_pricing_login' => array('label' => __('Role-Pricing Login Message', 'ai-eshop-optimizer'),'sel' => '.aieo-dmm-bridge--role_pricing',       'zoneable' => true),
            'brand_display'      => array('label' => __('Brand Display', 'ai-eshop-optimizer'),            'sel' => '.aieo-dmm-bridge--brand_display',      'zoneable' => true),
            'payment_logos'      => array('label' => __('Payment Logos', 'ai-eshop-optimizer'),            'sel' => '.aieo-dmm-bridge--payment_logos',      'zoneable' => true),
            'trust_badges'       => array('label' => __('Trust Badges', 'ai-eshop-optimizer'),             'sel' => '.aieo-dmm-bridge--trust_badges',       'zoneable' => true),
            'product_needs'      => array('label' => __('Product Needs', 'ai-eshop-optimizer'),            'sel' => '.aieo-dmm-bridge--product_needs',      'zoneable' => true),
            'complementary'      => array('label' => __('Complementary Products', 'ai-eshop-optimizer'),   'sel' => '.aieo-dmm-bridge--complementary',      'zoneable' => true),
            'waitlist'           => array('label' => __('Back-in-Stock Waitlist', 'ai-eshop-optimizer'),   'sel' => '.aieo-dmm-bridge--waitlist',           'zoneable' => true),
            'brand_gifts'        => array('label' => __('Free Brand Gifts', 'ai-eshop-optimizer'),         'sel' => '.aieo-dmm-bridge--brand_gifts',        'zoneable' => true),
            
            
            
            
            
            
            'wishlist'           => array('label' => __('Add to Wishlist', 'ai-eshop-optimizer'),          'sel' => '.aieo-wishlist-heart-wrap',           'zoneable' => true),
        );
    }

    
    public static function get_layout_order() {
        $defaults = array_keys(self::layout_catalog());
        $stored   = get_option(self::LAYOUT_ORDER_OPTION, array());
        if (!is_array($stored) || empty($stored)) { return $defaults; }
        $known = array_values(array_intersect($stored, $defaults));
        foreach ($defaults as $id) { if (!in_array($id, $known, true)) { $known[] = $id; } }
        return $known;
    }

    
    public static function get_cluster_zones() {
        $stored = get_option(self::ZONES_OPTION, array());
        if (!is_array($stored)) { $stored = array(); }
        $out = array();
        
        foreach (self::orderable_cluster_ids() as $id) {
            if ($id === 'wishlist' || $id === 'brand_display') {
                if (isset($stored[$id])) {
                    $z   = (string) $stored[$id];
                    $def = ($id === 'wishlist') ? 'beside_title' : 'info';
                    $out[$id] = in_array($z, array('info', 'under', 'beside_title'), true) ? $z : $def;
                } elseif ($id === 'wishlist') {
                    $out[$id] = 'beside_title';
                } else {
                    
                    
                    
                    
                    $bopt = get_option('aieo_dmm_brand_display_settings', array());
                    $bpos = (is_array($bopt) && isset($bopt['position'])) ? (string) $bopt['position'] : 'under_title';
                    $out[$id] = in_array($bpos, array('in_title_row', 'mobile_before_desktop_inline'), true) ? 'beside_title' : 'info';
                }
            } else {
                $out[$id] = (isset($stored[$id]) && $stored[$id] === 'under') ? 'under' : 'info';
            }
        }
        return $out;
    }

    public function ajax_save_order() {
        AIEO_DMM::verify_ajax_request();
        $raw = isset($_POST['order']) ? wp_unslash($_POST['order']) : array();
        if (!is_array($raw)) { $raw = array(); }
        
        $allowed = array_keys(self::layout_catalog());
        $clean = array();
        foreach ($raw as $id) {
            $id = sanitize_key($id);
            if (in_array($id, $allowed, true) && !in_array($id, $clean, true)) {
                $clean[] = $id;
            }
        }
        if (empty($clean)) {
            wp_send_json_error(array('message' => __('Empty order payload.', 'ai-eshop-optimizer')));
        }
        update_option(self::LAYOUT_ORDER_OPTION, $clean, false);

        
        
        if (isset($_POST['zones'])) {
            $catalog = self::layout_catalog();
            $rawz    = is_array($_POST['zones']) ? wp_unslash($_POST['zones']) : array();
            $zones   = array();
            foreach ($rawz as $id => $z) {
                $id = sanitize_key((string) $id);
                if (isset($catalog[$id]) && !empty($catalog[$id]['zoneable'])) {
                    $z = sanitize_key((string) $z);
                    $bt_def = array('wishlist' => 'beside_title', 'brand_display' => 'info');
                    if (isset($bt_def[$id])) {
                        $zones[$id] = in_array($z, array('info', 'under', 'beside_title'), true) ? $z : $bt_def[$id];
                    } else {
                        $zones[$id] = ($z === 'under') ? 'under' : 'info';
                    }
                }
            }
            update_option(self::ZONES_OPTION, $zones, false);
        }

        wp_send_json_success(array(
            'order'   => $clean,
            'message' => __('Layout saved.', 'ai-eshop-optimizer'),
        ));
    }

    
    private function cluster_block_names() {
        return array('aieo/post-meta', 'aieo/dmm-bridge');
    }

    
    public function wrap_cluster_block_in_template($block_content, $block) {
        if ( ! function_exists('is_product') || ! is_product() ) { return $block_content; }
        $name = isset($block['blockName']) ? (string) $block['blockName'] : '';
        if ( ! in_array($name, $this->cluster_block_names(), true) ) { return $block_content; }

        
        
        
        
        
        
        if ($name === 'aieo/dmm-bridge') {
            $mid = isset($block['attrs']['moduleId']) ? (string) $block['attrs']['moduleId'] : '';
            
            
            
            
            
            
            
            $cluster_bridge_ids = array('brand_display', 'payment_logos', 'trust_badges', 'product_needs', 'complementary', 'waitlist', 'brand_gifts');
            if ( ! in_array($mid, $cluster_bridge_ids, true) ) { return $block_content; }
            if ($mid === 'brand_display') {
                $method = isset($block['attrs']['renderMethod']) ? (string) $block['attrs']['renderMethod'] : 'output';
                if ($method !== 'render_after_post_meta') { return $block_content; }
            }
        }

        
        
        
        if ( trim($block_content) === '' ) { return $block_content; }

        
        
        
        if ( ! preg_match('/^(\s*)(<div\b[^>]*)(>)(.*)(<\/div>\s*)$/s', $block_content, $m) ) {
            return $block_content;
        }

        $lead       = $m[1];
        $open_tag   = $m[2]; 
        $inner      = $m[4];
        $close_tag  = $m[5]; 

        
        
        
        if ( preg_match('/class\s*=\s*"([^"]*)"/', $open_tag) ) {
            $open_tag = preg_replace(
                '/class\s*=\s*"([^"]*)"/',
                'class="$1 aieo-cluster-lazy"',
                $open_tag,
                1
            );
        } else {
            $open_tag .= ' class="aieo-cluster-lazy"';
        }
        $open_tag .= ' data-aieo-cluster-mount="1"';

        return $lead
            . $open_tag . '>'
            . '<template class="aieo-cluster-tpl">' . $inner . '</template>'
            . $close_tag;
    }

    
    public function enqueue_gallery_css() {
        if (!function_exists('is_product') || !is_product()) { return; }
        if (!defined('AIEO_FILE')) { return; }
        $rel  = 'assets/css/aieo-pdp-gallery.css';
        $path = dirname(AIEO_FILE) . '/' . $rel;
        $ver  = file_exists($path) ? filemtime($path) : (defined('AIEO_VERSION') ? AIEO_VERSION : '1.0');
        wp_enqueue_style('aieo-pdp-gallery', plugins_url($rel, AIEO_FILE), array(), $ver);
        
        
        
        $jsrel  = 'assets/js/aieo-pdp-gallery.js';
        $jspath = dirname(AIEO_FILE) . '/' . $jsrel;
        $jsver  = file_exists($jspath) ? filemtime($jspath) : $ver;
        wp_enqueue_script('aieo-pdp-gallery', plugins_url($jsrel, AIEO_FILE), array('jquery'), $jsver, true);
    }

    
    public function gallery_image_sizes($params, $attachment_id = 0, $image_size = '', $main_image = false) {
        if (is_array($params) && function_exists('is_product') && is_product()) {
            $params['sizes'] = '(max-width: 991px) 100vw, 48vw';
        }
        return $params;
    }

    
    public function enqueue_cluster_order_css() {
        if (!function_exists('is_product') || !is_product()) { return; }
        
        
        
        
        
        
        
        $catalog = self::layout_catalog();
        $order   = self::get_layout_order();
        $zones   = self::get_cluster_zones();
        $col     = '.wp-block-column.ff-product-info';
        $under   = '.wp-block-group.ff-product-under-image-cluster';

        $rules = array(
            $col   . '{display:flex !important;flex-direction:column !important;}',
            $col   . ' > .wp-block-group.ff-product-display-cluster{display:contents !important;}',
            $under . '{display:flex;flex-direction:column;}',
        );

        
        $title_ord = 10;
        foreach ($order as $i => $id) { if ($id === 'title') { $title_ord = ($i + 1) * 10; } }

        
        
        
        $brand_pos = '';
        if (class_exists('AIEO_DMM_Brand_Display')) {
            $bs = (new AIEO_DMM_Brand_Display())->get_settings();
            $brand_pos = isset($bs['position']) ? (string) $bs['position'] : '';
        }
        
        
        
        
        
        
        
        
        
        
        $brand_glued = in_array($brand_pos, array('in_title_row'), true);

        $any_under = false;
        foreach ($order as $i => $id) {
            if (!isset($catalog[$id])) { continue; }
            $sel = (string) $catalog[$id]['sel'];
            $ord = ($i + 1) * 10;
            $zoneable = !empty($catalog[$id]['zoneable']);
            $zone = ($zoneable && isset($zones[$id]) && $zones[$id] === 'under') ? 'under' : 'info';

            if ($id === 'brand_display' && $brand_glued) {
                
                
                
                $rules[] = sprintf('%s .aieo-dmm-bridge--brand_display{order:%d !important;}', $col, $title_ord - 1);
                continue;
            }
            if ($zone === 'under') {
                $any_under = true;
                $rules[] = sprintf('%s > %s{order:%d !important;}', $under, $sel, $ord);
                continue;
            }
            
            
            
            $rules[] = sprintf('%s %s{order:%d !important;}', $col, $sel, $ord);
            if ($id === 'title') {
                
                
                $rules[] = sprintf('%s > .ff-product-title-row{order:%d !important;}', $col, $ord);
            }
        }
        
        if (!$any_under) { $rules[] = $under . '{display:none !important;}'; }

        $css = implode('', $rules);
        
        
        wp_register_style('aieo-dmm-product-display-order', false, array(), defined('AIEO_VERSION') ? AIEO_VERSION : '1.0');
        wp_enqueue_style('aieo-dmm-product-display-order');
        wp_add_inline_style('aieo-dmm-product-display-order', $css);

        
        
        
        
        
        wp_register_script('aieo-dmm-cluster-hydrate', false, array(), defined('AIEO_VERSION') ? AIEO_VERSION : '1.0', true);
        wp_enqueue_script('aieo-dmm-cluster-hydrate');
        wp_add_inline_script('aieo-dmm-cluster-hydrate', $this->cluster_hydrate_js());
    }

    
    private function cluster_hydrate_js() {
        return <<<'JS'
(function(){
    "use strict";
    function hydrateOne(mount) {
        if (mount.__aieoHydrated) return;
        var tpl = mount.querySelector(":scope > template.aieo-cluster-tpl");
        if (!tpl || !tpl.content) return;
        mount.__aieoHydrated = true;
        mount.appendChild(tpl.content.cloneNode(true));
        if (tpl.parentNode) tpl.parentNode.removeChild(tpl);
        try {
            mount.dispatchEvent(new CustomEvent("aieo:cluster:hydrated", { bubbles: true, detail: { mount: mount } }));
        } catch (e) {}
    }
    function init() {
        // Scan the whole document for lazy mounts. Originally this was
        // scoped to `.ff-product-display-cluster` (4 modules render
        // inside that wrapper), but lazy markers now also appear OUTSIDE
        // the cluster — e.g. complementary's picker is hooked into
        // `woocommerce_before_add_to_cart_button` and lands inside WC's
        // add-to-cart-form, not the cluster. Document-wide scan keeps
        // the hydrator the single source of truth regardless of where
        // a module's `data-aieo-cluster-mount` lands.
        var mounts = document.querySelectorAll("[data-aieo-cluster-mount]");
        if (!mounts.length) return;
        // No IO support → hydrate immediately (graceful degradation).
        if (!("IntersectionObserver" in window)) {
            mounts.forEach(hydrateOne);
            return;
        }
        // Per-mount observer: each mount hydrates only when ITS region
        // nears the viewport. Cheaper than waiting for one wrapper to
        // intersect (which would over-hydrate when mounts are scattered
        // across the page) and lets above-fold mounts hydrate even
        // before any user scroll.
        mounts.forEach(function(mount) {
            var io = new IntersectionObserver(function(entries) {
                for (var i = 0; i < entries.length; i++) {
                    if (entries[i].isIntersecting) {
                        hydrateOne(mount);
                        io.disconnect();
                        return;
                    }
                }
            }, { rootMargin: "200px 0px" });
            io.observe(mount);
        });
    }
    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", init);
    } else {
        init();
    }
})();
JS;
    }

    
    
    

    
    public static function reco_catalog() {
        $built_in = array(
            'same_needs'      => __('Products for the same needs', 'ai-eshop-optimizer'),
            'crosssells'      => __('Cross-sells', 'ai-eshop-optimizer'),
            'upsells'         => __('Up-sells', 'ai-eshop-optimizer'),
            'related'         => __('Related (by WooCommerce)', 'ai-eshop-optimizer'),
            'recently_viewed' => __('Recently Viewed', 'ai-eshop-optimizer'),
        );
        $catalog = array();
        foreach ($built_in as $id => $label) {
            $catalog[] = array(
                'id'    => $id,
                'label' => $label,
                'kind'  => 'recos',
            );
        }
        if (class_exists('AIEO_DMM_Custom_Slots')) {
            foreach (AIEO_DMM_Custom_Slots::get_all_slots() as $slot) {
                $catalog[] = array(
                    'id'         => (string) $slot['id'],
                    'label'      => $slot['name'] !== '' ? (string) $slot['name'] : (string) $slot['id'],
                    'kind'       => 'slot',
                    'slot_type'  => (string) $slot['type'],
                    'slot_enabled' => !empty($slot['enabled']),
                );
            }
        }
        return $catalog;
    }

    
    public static function get_reco_order() {
        $catalog = self::reco_catalog();
        $by_id   = array();
        foreach ($catalog as $c) { $by_id[$c['id']] = $c; }

        $stored = get_option(self::RECO_ORDER_OPTION, null);
        $is_first_run = !is_array($stored);
        if (!is_array($stored)) { $stored = array(); }

        $seen = array();
        $out  = array();
        foreach ($stored as $row) {
            if (!is_array($row) || empty($row['id'])) { continue; }
            $id = (string) $row['id'];
            if (!isset($by_id[$id]) || isset($seen[$id])) { continue; }
            $seen[$id] = true;
            $out[] = array(
                'id'      => $id,
                'visible' => !empty($row['visible']),
            );
        }
        
        
        
        
        
        
        $legacy_keys = self::builtin_reco_legacy_visibility_keys();
        foreach ($catalog as $c) {
            if (isset($seen[$c['id']])) { continue; }
            $default_visible = true;
            if ($is_first_run && $c['kind'] === 'recos' && isset($legacy_keys[$c['id']])) {
                $default_visible = (bool) get_option($legacy_keys[$c['id']]);
            }
            $out[] = array(
                'id'      => $c['id'],
                'visible' => $default_visible,
            );
        }
        return $out;
    }

    
    public function ajax_save_reco_order() {
        AIEO_DMM::verify_ajax_request();

        $raw_order = isset($_POST['order']) ? wp_unslash($_POST['order']) : array();
        if (!is_array($raw_order)) { $raw_order = array(); }

        $raw_visible = isset($_POST['visible']) ? wp_unslash($_POST['visible']) : array();
        if (!is_array($raw_visible)) { $raw_visible = array(); }

        $catalog = self::reco_catalog();
        $allowed = array();
        foreach ($catalog as $c) { $allowed[$c['id']] = true; }

        $seen = array();
        $rows = array();
        foreach ($raw_order as $id) {
            $id = sanitize_key($id);
            if ($id === '' || !isset($allowed[$id]) || isset($seen[$id])) { continue; }
            $seen[$id] = true;
            
            
            $v = isset($raw_visible[$id]) ? (string) $raw_visible[$id] : '0';
            $rows[] = array(
                'id'      => $id,
                'visible' => in_array($v, array('1', 'true', 'on', 'yes'), true),
            );
        }
        if (empty($rows)) {
            wp_send_json_error(array('message' => __('Empty order payload.', 'ai-eshop-optimizer')));
        }
        update_option(self::RECO_ORDER_OPTION, $rows, false);
        wp_send_json_success(array(
            'order'   => $rows,
            'message' => __('Recommendations order saved.', 'ai-eshop-optimizer'),
        ));
    }

    
    public static function tab_catalog() {
        $catalog = array(
            array( 'id' => 'description',            'label' => __( 'Description', 'ai-eshop-optimizer' ),            'kind' => 'core' ),
            array( 'id' => 'additional_information', 'label' => __( 'Additional information', 'ai-eshop-optimizer' ), 'kind' => 'core' ),
            array( 'id' => 'reviews',                'label' => __( 'Reviews', 'ai-eshop-optimizer' ),                'kind' => 'core' ),
        );
        if ( class_exists( 'AIEO_DMM_Custom_Tabs' ) ) {
            foreach ( AIEO_DMM_Custom_Tabs::get_enabled_slots() as $i => $slot ) {
                $label = ( isset( $slot['label'] ) && $slot['label'] !== '' )
                    ? (string) $slot['label']
                    
                    : sprintf( __( 'Custom Tab %d', 'ai-eshop-optimizer' ), (int) $i );
                $catalog[] = array(
                    'id'    => 'aieo_custom_tab_' . (int) $i,
                    'label' => $label,
                    'kind'  => 'custom',
                );
            }
        }
        return $catalog;
    }

    
    public static function get_tab_order() {
        $catalog = self::tab_catalog();
        $allowed = array();
        foreach ( $catalog as $c ) { $allowed[ $c['id'] ] = true; }

        $stored = get_option( self::TAB_ORDER_OPTION, null );
        if ( ! is_array( $stored ) ) { $stored = array(); }

        $seen = array();
        $out  = array();
        foreach ( $stored as $id ) {
            $id = (string) $id;
            if ( ! isset( $allowed[ $id ] ) || isset( $seen[ $id ] ) ) { continue; }
            $seen[ $id ] = true;
            $out[]       = $id;
        }
        foreach ( $catalog as $c ) {
            if ( isset( $seen[ $c['id'] ] ) ) { continue; }
            $out[] = $c['id'];
        }
        return $out;
    }

    
    public function apply_tab_order( $tabs ) {
        if ( ! is_array( $tabs ) || empty( $tabs ) ) { return $tabs; }
        $order = get_option( self::TAB_ORDER_OPTION, null );
        if ( ! is_array( $order ) || empty( $order ) ) { return $tabs; }

        $base = 10;
        $seen = array();
        foreach ( $order as $id ) {
            $id = (string) $id;
            if ( isset( $tabs[ $id ] ) && ! isset( $seen[ $id ] ) ) {
                $tabs[ $id ]['priority'] = $base;
                $base += 10;
                $seen[ $id ] = true;
            }
        }
        
        
        foreach ( $tabs as $id => $tab ) {
            if ( ! isset( $seen[ $id ] ) ) {
                $tabs[ $id ]['priority'] = $base;
                $base += 10;
            }
        }
        return $tabs;
    }

    
    public function ajax_save_tab_order() {
        AIEO_DMM::verify_ajax_request();

        $raw = isset( $_POST['order'] ) ? wp_unslash( $_POST['order'] ) : array();
        if ( ! is_array( $raw ) ) { $raw = array(); }

        $catalog = self::tab_catalog();
        $allowed = array();
        foreach ( $catalog as $c ) { $allowed[ $c['id'] ] = true; }

        $seen = array();
        $rows = array();
        foreach ( $raw as $id ) {
            
            $id = preg_replace( '/[^a-z0-9_]/', '', (string) $id );
            if ( $id === '' || ! isset( $allowed[ $id ] ) || isset( $seen[ $id ] ) ) { continue; }
            $seen[ $id ] = true;
            $rows[]      = $id;
        }
        if ( empty( $rows ) ) {
            wp_send_json_error( array( 'message' => __( 'Empty order payload.', 'ai-eshop-optimizer' ) ) );
        }
        update_option( self::TAB_ORDER_OPTION, $rows, false );
        wp_send_json_success( array(
            'order'   => $rows,
            'message' => __( 'Tab order saved.', 'ai-eshop-optimizer' ),
        ) );
    }

    
    public function reorder_classic_theme_recos() {
        
        
        
        if (function_exists('wp_is_block_theme') && wp_is_block_theme()) { return; }

        if (!function_exists('is_singular') || !is_singular('product')) { return; }

        global $wp_filter;
        if (empty($wp_filter['woocommerce_after_single_product_summary'])) { return; }

        $hook  = $wp_filter['woocommerce_after_single_product_summary'];
        $order = self::get_reco_order();
        if (empty($order)) { return; }

        
        
        
        $builtin_methods = array(
            'same_needs'      => 'aieo_show_same_needs_placeholder',
            'crosssells'      => 'aieo_show_cross_sell_placeholder',
            'upsells'         => 'aieo_show_upsell_placeholder',
            'related'         => 'aieo_show_related_placeholder',
            'recently_viewed' => 'aieo_show_recently_viewed_placeholder',
        );

        
        
        $found_builtins = array();   
        $found_slots_cb = array();   
        foreach ($hook->callbacks as $prio => $cbs) {
            foreach ($cbs as $cb) {
                $fn = isset($cb['function']) ? $cb['function'] : null;
                if (!is_array($fn) || !is_object($fn[0])) { continue; }
                $method = (string) $fn[1];
                $class  = get_class($fn[0]);

                
                foreach ($builtin_methods as $sid => $name) {
                    if ($method === $name && !isset($found_builtins[$sid])) {
                        $found_builtins[$sid] = array('priority' => $prio, 'callback' => $fn);
                    }
                }

                
                
                
                if ($class === 'AIEO_DMM_Custom_Slots' && $method === 'render_classic_theme_region') {
                    $found_slots_cb[] = array('priority' => $prio, 'callback' => $fn);
                }
            }
        }

        
        
        
        foreach ($found_slots_cb as $entry) {
            remove_action('woocommerce_after_single_product_summary', $entry['callback'], $entry['priority']);
        }

        
        
        
        $new_prio = 10;
        foreach ($order as $row) {
            $id      = isset($row['id']) ? (string) $row['id'] : '';
            if ($id === '') { continue; }
            $visible = !empty($row['visible']);

            if (isset($found_builtins[$id])) {
                
                $entry = $found_builtins[$id];
                remove_action('woocommerce_after_single_product_summary', $entry['callback'], $entry['priority']);
                if ($visible) {
                    add_action('woocommerce_after_single_product_summary', $entry['callback'], $new_prio);
                }
                $new_prio += 10;
                continue;
            }

            
            
            
            
            
            if (strpos($id, 'slot_') === 0 && $visible) {
                $sid = $id; 
                add_action('woocommerce_after_single_product_summary', static function () use ($sid) {
                    if (!class_exists('AIEO_DMM_Custom_Slots')) { return; }
                    $html = AIEO_DMM_Custom_Slots::render_slot($sid);
                    if ($html === '') { return; }
                    $clean = esc_attr(sanitize_html_class($sid));
                    echo '<section class="aieo-custom-slot aieo-custom-slot--' . $clean 
                        . ' aieo-recommendations" data-aieo-reco="' . $clean
                        . '" data-section="' . $clean . '">' . $html . '</section>';
                }, $new_prio);
            }
            $new_prio += 10;
        }
    }

    
    public static function classic_summary_callback_map() {
        return array(
            'title'              => array(array('fn', 'woocommerce_template_single_title')),
            
            
            'rating'             => array(
                array('fn', 'woocommerce_template_single_rating'),
                array('method', 'AIEO_Ratings', 'render_single_widget'),
            ),
            'summary'            => array(array('fn', 'woocommerce_template_single_excerpt')),
            'price'              => array(array('fn', 'woocommerce_template_single_price')),
            'add_to_cart'        => array(array('fn', 'woocommerce_template_single_add_to_cart')),
            'brand_display'      => array(
                array('method', 'AIEO_DMM_Brand_Display', 'output_classic_before_title'),
                array('method', 'AIEO_DMM_Brand_Display', 'output_classic_under_title'),
            ),
            'brand_gifts'        => array(array('method', 'AIEO_DMM_Brand_Gifts', 'render_single_product_section')),
            'payment_logos'      => array(array('method', 'AIEO_DMM_Payment_Logos', 'output')),
            'trust_badges'       => array(array('method', 'AIEO_DMM_Trust_Badges', 'output')),
            'product_meta'       => array(array('method', 'AIEO_DMM_Product_Meta', 'output')),
            'product_categories' => array(array('method', 'AIEO_DMM_Product_Categories', 'output')),
        );
    }

    
    public function reorder_classic_theme_summary() {
        if (function_exists('wp_is_block_theme') && wp_is_block_theme()) { return; }
        if (!function_exists('is_singular') || !is_singular('product')) { return; }

        global $wp_filter;
        if (empty($wp_filter['woocommerce_single_product_summary'])) { return; }

        $map  = self::classic_summary_callback_map();
        $hook = $wp_filter['woocommerce_single_product_summary'];

        
        
        $found = array(); 
        foreach ($hook->callbacks as $prio => $cbs) {
            foreach ($cbs as $cb) {
                $fn = isset($cb['function']) ? $cb['function'] : null;
                if ($fn === null) { continue; }
                $aa = isset($cb['accepted_args']) ? (int) $cb['accepted_args'] : 1;
                foreach ($map as $id => $matchers) {
                    foreach ($matchers as $m) {
                        $hit = false;
                        if ($m[0] === 'fn') {
                            if (is_string($fn) && $fn === $m[1]) { $hit = true; }
                        } elseif ($m[0] === 'method') {
                            if (is_array($fn) && isset($fn[0], $fn[1]) && (string) $fn[1] === $m[2]) {
                                $cls = is_object($fn[0]) ? get_class($fn[0]) : (is_string($fn[0]) ? $fn[0] : '');
                                if ($cls === $m[1]) { $hit = true; }
                            }
                        }
                        if ($hit) {
                            $found[$id][] = array('priority' => (int) $prio, 'callback' => $fn, 'accepted_args' => $aa);
                        }
                    }
                }
            }
        }
        if (empty($found)) { return; }

        
        
        
        $order = self::get_layout_order();
        $rank  = 0;
        foreach ($order as $id) {
            if (empty($found[$id])) { continue; }
            $new_prio = 5 + ($rank * 4);
            foreach ($found[$id] as $entry) {
                if ((int) $entry['priority'] !== $new_prio) {
                    remove_action('woocommerce_single_product_summary', $entry['callback'], $entry['priority']);
                    add_action('woocommerce_single_product_summary', $entry['callback'], $new_prio, (int) $entry['accepted_args']);
                }
            }
            $rank++;
        }
    }

    
    public function enqueue_recommendations_order_css() {
        if (!function_exists('is_product') || !is_product()) { return; }
        $order = self::get_reco_order();
        if (empty($order)) { return; }

        $rules = array(
            '.wp-block-group.ff-recommendations-cluster{display:flex !important;flex-direction:column !important;}',
        );
        foreach ($order as $i => $row) {
            $selector = '.wp-block-group.ff-recommendations-cluster > [data-aieo-reco="' . esc_attr($row['id']) . '"]';
            $rules[]  = sprintf('%s{order:%d;}', $selector, ($i + 1) * 10);
            if (empty($row['visible'])) {
                $rules[] = sprintf('%s{display:none !important;}', $selector);
            }
        }
        $css = implode('', $rules);

        wp_register_style('aieo-dmm-recommendations-order', false, array(), defined('AIEO_VERSION') ? AIEO_VERSION : '1.0');
        wp_enqueue_style('aieo-dmm-recommendations-order');
        wp_add_inline_style('aieo-dmm-recommendations-order', $css);
    }

    public function render_subtab() {
        $catalog = $this->modules_catalog();
        $nonce = wp_create_nonce(AIEO_DMM::NONCE_ACTION);
        $ajax  = admin_url('admin-ajax.php');
        $base_admin_url = admin_url('admin.php?page=' . AIEO_DMM::MENU_SLUG . '&dmm_tab=');

        echo '<div class="aieo-dmm-section">';
        echo '<h2>' . esc_html__('Product Display', 'ai-eshop-optimizer') . '</h2>';
        echo '<p class="description">' . esc_html__('Central control panel for product-page features. Flip the switch to enable a feature globally; click "Configure" to open its dedicated settings page.', 'ai-eshop-optimizer') . '</p>';

        echo '<table class="wp-list-table widefat fixed striped" style="margin-top:12px;">';
        echo '<thead><tr>';
        echo '<th style="width:60px;">' . esc_html__('On', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:200px;">' . esc_html__('Module', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('Description', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:120px;text-align:right;"></th>';
        echo '</tr></thead><tbody>';

        foreach ($catalog as $m) {
            $on = self::module_is_enabled($m['option_key']);
            $configure_url = esc_url($base_admin_url . $m['id']);
            echo '<tr data-module="' . esc_attr($m['id']) . '">';
            echo '<td>';
            echo '<label class="aieo-dmm-toggle" style="margin:0;">';
            echo '<input type="checkbox" data-aieo-pd-toggle="' . esc_attr($m['id']) . '"' . ($on ? ' checked' : '') . '>';
            echo '<span class="aieo-dmm-toggle-slider"></span>';
            echo '</label>';
            echo '</td>';
            echo '<td><strong>' . esc_html($m['label']) . '</strong></td>';
            echo '<td>' . esc_html($m['description']) . '</td>';
            echo '<td style="text-align:right;"><a href="' . $configure_url . '" class="button button-secondary">' . esc_html__('Configure', 'ai-eshop-optimizer') . ' →</a></td>'; 
            echo '</tr>';
        }
        echo '</tbody></table>';

        echo '<p id="aieo-pd-status" style="margin:10px 0 0;color:#646970;font-size:13px;"></p>';

        
        
        
        
        
        
        
        
        
        $order      = self::get_cluster_order();
        $catalog_by = array();
        foreach ($catalog as $m) { $catalog_by[$m['id']] = $m; }

        
        
        
        
        
        echo '<div id="aieo-card-display-order" class="aieo-dmm-card" style="margin-top:24px;">';
        echo '<h3 style="margin-top:0;">' . esc_html__('Product info-column layout — order & placement', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description" style="margin-bottom:12px;">' . esc_html__('Drag to set the vertical order of EVERYTHING in the single-product info column — the core elements (Title, Price, Add-to-Cart, messages) and the modules (SKU, badges, categories…), in one sequence. Modules also have a “Beside / Under image” dropdown to move them under the gallery; core elements always sit beside the image.', 'ai-eshop-optimizer') . '</p>';
        $layout_order = self::get_layout_order();
        $lcat  = self::layout_catalog();
        $zones = self::get_cluster_zones();
        echo '<ul id="aieo-pd-order-list" style="margin:0;padding:0;max-width:680px;list-style:none;">';
        foreach ($layout_order as $id) {
            if (!isset($lcat[$id])) { continue; }
            $L        = $lcat[$id];
            $zoneable = !empty($L['zoneable']);
            $zone     = ($zoneable && isset($zones[$id])) ? $zones[$id] : 'info';
            $is_mod   = isset($catalog_by[$id]);
            if ($id === 'wishlist') {
                
                
                $wlopt = get_option('aieo_wishlist_display', array());
                $on    = (!is_array($wlopt) || !isset($wlopt['show_on_single'])) ? true : ($wlopt['show_on_single'] === 'yes');
            } else {
                $on   = $is_mod ? self::module_is_enabled($catalog_by[$id]['option_key']) : null;
            }
            echo '<li class="aieo-pd-order-item" data-id="' . esc_attr($id) . '" data-kind="' . ($zoneable ? 'module' : 'core') . '" style="display:flex;align-items:center;gap:10px;padding:10px 12px;margin:0 0 6px;background:' . ($zoneable ? '#fff' : '#f8fafc') . ';border:1px solid #e5e7eb;border-radius:4px;cursor:grab;">';
            echo '<span class="dashicons dashicons-menu" style="color:#9ca3af;"></span>';
            echo '<strong style="flex:1;">' . esc_html($L['label']) . '</strong>';
            if ($zoneable) {
                echo '<select class="aieo-pd-zone" data-id="' . esc_attr($id) . '" style="font-size:12px;max-width:175px;">';
                echo '<option value="info"' . selected($zone, 'info', false) . '>' . esc_html__('Beside image', 'ai-eshop-optimizer') . '</option>';
                if ($id === 'wishlist' || $id === 'brand_display') {
                    
                    echo '<option value="beside_title"' . selected($zone, 'beside_title', false) . '>' . esc_html__('Beside the product title', 'ai-eshop-optimizer') . '</option>';
                }
                echo '<option value="under"' . selected($zone, 'under', false) . '>' . esc_html__('Under image', 'ai-eshop-optimizer') . '</option>';
                echo '</select>';
                echo '<span style="font-size:11px;width:54px;text-align:right;color:' . ($on ? '#15803d' : '#9ca3af') . ';">' . esc_html($on ? __('enabled', 'ai-eshop-optimizer') : __('disabled', 'ai-eshop-optimizer')) . '</span>';
            } else {
                echo '<span style="font-size:11px;color:#9ca3af;width:204px;text-align:right;">' . esc_html__('core element', 'ai-eshop-optimizer') . '</span>';
            }
            echo '</li>';
        }
        echo '</ul>';
        echo '<p style="margin:10px 0 0;display:flex;align-items:center;gap:12px;"><button type="button" class="button" id="aieo-pd-order-reset">' . esc_html__('Reset to default', 'ai-eshop-optimizer') . '</button><span id="aieo-pd-order-status" style="color:#646970;font-size:13px;"></span></p>';
        echo '</div>';
        
        
        ?>
        <script>
        (function(){
            var rst=document.getElementById("aieo-pd-order-reset"); if(!rst) return;
            rst.addEventListener("click",function(){
                var fd=new FormData(); fd.set("action","aieo_dmm_product_display_save_order");
                fd.set("nonce",<?php echo wp_json_encode(wp_create_nonce(AIEO_DMM::NONCE_ACTION)); ?>);
                <?php foreach (array_keys($lcat) as $lid) { echo 'fd.append("order[]",' . wp_json_encode($lid) . ');'; } ?>
                fetch(<?php echo wp_json_encode(admin_url('admin-ajax.php')); ?>,{method:"POST",credentials:"same-origin",body:fd})
                  .then(function(){ setTimeout(function(){location.reload();},400); });
            });
        })();
        </script>
        <?php

        
        
        
        
        
        
        
        
        $tab_order   = self::get_tab_order();
        $tab_catalog = self::tab_catalog();
        $tab_by_id   = array();
        foreach ($tab_catalog as $c) { $tab_by_id[$c['id']] = $c; }
        $has_custom_tabs = false;
        foreach ($tab_catalog as $c) { if ($c['kind'] === 'custom') { $has_custom_tabs = true; break; } }

        echo '<div id="aieo-card-tab-order" class="aieo-dmm-card" style="margin-top:24px;">';
        echo '<h3 style="margin-top:0;">' . esc_html__('Product Tab Ordering', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description" style="margin-bottom:12px;">' . esc_html__('Drag to set the order of the tabs under each product — the WooCommerce tabs (Description, Additional information, Reviews) and your Custom Tabs, all in one sequence. Tabs that have no content for a given product (e.g. an empty custom tab, or Reviews when reviews are off) are simply skipped — the order still holds for the ones that show.', 'ai-eshop-optimizer') . '</p>';
        if (!$has_custom_tabs) {
            echo '<p class="description" style="margin:0 0 12px;color:#9ca3af;">' . wp_kses_post(sprintf(
                
                __('Enable a tab under %s to add it to this list.', 'ai-eshop-optimizer'),
                '<em>' . esc_html__('Custom Tabs', 'ai-eshop-optimizer') . '</em>'
            )) . '</p>';
        }
        echo '<ul id="aieo-tab-order-list" style="margin:0;padding:0;max-width:560px;list-style:none;">';
        foreach ($tab_order as $id) {
            if (!isset($tab_by_id[$id])) { continue; }
            $c     = $tab_by_id[$id];
            $badge = ($c['kind'] === 'custom')
                ? '<span style="font-size:11px;color:#15803d;">' . esc_html__('custom tab', 'ai-eshop-optimizer') . '</span>'
                : '<span style="font-size:11px;color:#9ca3af;">' . esc_html__('WooCommerce', 'ai-eshop-optimizer') . '</span>';
            echo '<li class="aieo-tab-order-item" data-id="' . esc_attr($id) . '" style="display:flex;align-items:center;gap:10px;padding:10px 12px;margin:0 0 6px;background:#fff;border:1px solid #e5e7eb;border-radius:4px;cursor:grab;">';
            echo '<span class="dashicons dashicons-menu" style="color:#9ca3af;"></span>';
            echo '<strong style="flex:1;">' . esc_html($c['label']) . '</strong>';
            echo $badge; 
            echo '</li>';
        }
        echo '</ul>';
        echo '<p id="aieo-tab-order-status" style="margin:10px 0 0;color:#646970;font-size:13px;"></p>';
        echo '</div>';
        ?>
        <script>
        (function(){
            var list = document.getElementById("aieo-tab-order-list");
            var stat = document.getElementById("aieo-tab-order-status");
            if (!list) { return; }
            list.querySelectorAll(".aieo-tab-order-item").forEach(function(li){
                li.setAttribute("draggable", "true");
                li.addEventListener("dragstart", function(e){
                    li.classList.add("is-dragging");
                    e.dataTransfer.effectAllowed = "move";
                    e.dataTransfer.setData("text/plain", li.dataset.id);
                });
                li.addEventListener("dragend", function(){
                    li.classList.remove("is-dragging");
                    list.querySelectorAll(".aieo-tab-order-item").forEach(function(n){ n.classList.remove("is-drag-over"); });
                });
                li.addEventListener("dragover", function(e){
                    e.preventDefault();
                    e.dataTransfer.dropEffect = "move";
                    li.classList.add("is-drag-over");
                });
                li.addEventListener("dragleave", function(){
                    li.classList.remove("is-drag-over");
                });
                li.addEventListener("drop", function(e){
                    e.preventDefault();
                    li.classList.remove("is-drag-over");
                    var dragging = list.querySelector(".aieo-tab-order-item.is-dragging");
                    if (!dragging || dragging === li) { return; }
                    var rect = li.getBoundingClientRect();
                    var above = (e.clientY - rect.top) < (rect.height / 2);
                    list.insertBefore(dragging, above ? li : li.nextSibling);
                    saveTabOrder();
                });
            });
            function saveTabOrder(){
                var ids = Array.prototype.map.call(
                    list.querySelectorAll(".aieo-tab-order-item"),
                    function(n){ return n.dataset.id; }
                );
                var fd = new FormData();
                fd.set("action", "aieo_dmm_save_tab_order");
                fd.set("nonce", <?php echo wp_json_encode($nonce); ?>);
                ids.forEach(function(id){ fd.append("order[]", id); });
                stat.style.color = "#646970";
                stat.textContent = <?php echo wp_json_encode(__('Saving order…', 'ai-eshop-optimizer')); ?>;
                fetch(<?php echo wp_json_encode($ajax); ?>, {method:"POST", credentials:"same-origin", body:fd})
                    .then(function(r){ return r.json(); })
                    .then(function(j){
                        if (j && j.success) {
                            stat.style.color = "#15803d";
                            stat.textContent = j.data.message;
                        } else {
                            stat.style.color = "#b91c1c";
                            stat.textContent = (j && j.data && j.data.message) || <?php echo wp_json_encode(__('Save failed.', 'ai-eshop-optimizer')); ?>;
                        }
                    });
            }
        })();
        </script>
        <?php

        
        
        
        
        
        
        
        $reco_order   = self::get_reco_order();
        $reco_catalog = self::reco_catalog();
        $reco_by_id   = array();
        foreach ($reco_catalog as $c) { $reco_by_id[$c['id']] = $c; }

        echo '<div id="aieo-card-recommendations-order" class="aieo-dmm-card" style="margin-top:24px;">';
        echo '<h3 style="margin-top:0;">' . esc_html__('Recommendations Ordering', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description" style="margin-bottom:12px;">' . esc_html__('Drag the rows to set the vertical order of the recommendation rails on every single-product page. Untick the checkbox to hide a rail without removing it. Custom slots you add in Custom Slots appear here automatically — drag them anywhere among the built-in rails.', 'ai-eshop-optimizer') . '</p>';
        echo '<ul id="aieo-reco-order-list" style="margin:0;padding:0;max-width:560px;list-style:none;">';
        foreach ($reco_order as $row) {
            $id = $row['id'];
            if (!isset($reco_by_id[$id])) { continue; }
            $c       = $reco_by_id[$id];
            $visible = !empty($row['visible']);
            
            
            
            $badge = '';
            if ($c['kind'] === 'slot') {
                $slot_on = !empty($c['slot_enabled']);
                $badge   = '<span style="font-size:11px;color:' . ($slot_on ? '#15803d' : '#9ca3af') . ';">' . esc_html(($c['slot_type'] === 'tolstoy' ? 'Tolstoy' : 'HTML') . ' · ' . ($slot_on ? __('configured', 'ai-eshop-optimizer') : __('not configured', 'ai-eshop-optimizer'))) . '</span>';
            } else {
                $badge = '<span style="font-size:11px;color:#9ca3af;">' . esc_html__('built-in', 'ai-eshop-optimizer') . '</span>';
            }
            echo '<li class="aieo-reco-order-item" data-id="' . esc_attr($id) . '" style="display:flex;align-items:center;gap:10px;padding:10px 12px;margin:0 0 6px;background:#fff;border:1px solid #e5e7eb;border-radius:4px;cursor:grab;">';
            echo '<span class="dashicons dashicons-menu" style="color:#9ca3af;"></span>';
            echo '<strong style="flex:1;">' . esc_html($c['label']) . '</strong>';
            echo $badge; 
            echo '<label style="display:inline-flex;align-items:center;gap:6px;font-size:12px;color:#374151;">';
            echo '<input type="checkbox" data-aieo-reco-visible="' . esc_attr($id) . '"' . checked($visible, true, false) . '> ' . esc_html__('Show', 'ai-eshop-optimizer');
            echo '</label>';
            echo '</li>';
        }
        echo '</ul>';
        echo '<p id="aieo-reco-order-status" style="margin:10px 0 0;color:#646970;font-size:13px;"></p>';
        echo '</div>';

        echo '<style>
            .aieo-dmm-toggle{position:relative;display:inline-block;width:44px;height:22px;}
            .aieo-dmm-toggle input{opacity:0;width:0;height:0;}
            .aieo-dmm-toggle-slider{position:absolute;cursor:pointer;inset:0;background:#cbd5e1;border-radius:22px;transition:.2s;}
            .aieo-dmm-toggle-slider:before{position:absolute;content:"";height:18px;width:18px;left:2px;bottom:2px;background:white;border-radius:50%;transition:.2s;}
            .aieo-dmm-toggle input:checked + .aieo-dmm-toggle-slider{background:#15803d;}
            .aieo-dmm-toggle input:checked + .aieo-dmm-toggle-slider:before{transform:translateX(22px);}
            .aieo-pd-order-item.is-dragging,.aieo-reco-order-item.is-dragging,.aieo-tab-order-item.is-dragging{opacity:0.4;}
            .aieo-pd-order-item.is-drag-over,.aieo-reco-order-item.is-drag-over,.aieo-tab-order-item.is-drag-over{border-color:#1565c0;background:#eff6ff;}
        </style>';

        echo '<script>(function(){
            var status = document.getElementById("aieo-pd-status");
            document.querySelectorAll("[data-aieo-pd-toggle]").forEach(function(input){
                input.addEventListener("change", function(){
                    var fd = new FormData();
                    fd.set("action", "aieo_dmm_product_display_toggle");
                    fd.set("nonce", "' . esc_js($nonce) . '");
                    fd.set("module", input.getAttribute("data-aieo-pd-toggle"));
                    fd.set("enabled", input.checked ? "1" : "0");
                    status.style.color = "#646970";
                    status.textContent = "' . esc_js(__('Saving…', 'ai-eshop-optimizer')) . '";
                    fetch("' . esc_js($ajax) . '", {method:"POST", credentials:"same-origin", body:fd})
                        .then(function(r){return r.json();})
                        .then(function(j){
                            if (j && j.success) {
                                status.style.color = "#15803d";
                                status.textContent = j.data.message;
                            } else {
                                status.style.color = "#b91c1c";
                                status.textContent = (j && j.data && j.data.message) || "' . esc_js(__('Failed.', 'ai-eshop-optimizer')) . '";
                                input.checked = !input.checked; // revert
                            }
                        });
                });
            });

            // ── Drag-and-drop reorder for the display-order card ──
            // Native HTML5 drag-and-drop — no jQuery UI dependency. Reads
            // the current DOM order on each drop and POSTs the new order
            // as an array of module ids to aieo_dmm_product_display_save_order.
            var list      = document.getElementById("aieo-pd-order-list");
            var orderStat = document.getElementById("aieo-pd-order-status");
            if (list) {
                list.querySelectorAll(".aieo-pd-order-item").forEach(function(li){
                    li.setAttribute("draggable", "true");
                    li.addEventListener("dragstart", function(e){
                        li.classList.add("is-dragging");
                        e.dataTransfer.effectAllowed = "move";
                        e.dataTransfer.setData("text/plain", li.dataset.id);
                    });
                    li.addEventListener("dragend", function(){
                        li.classList.remove("is-dragging");
                        list.querySelectorAll(".aieo-pd-order-item").forEach(function(n){ n.classList.remove("is-drag-over"); });
                    });
                    li.addEventListener("dragover", function(e){
                        e.preventDefault();
                        e.dataTransfer.dropEffect = "move";
                        li.classList.add("is-drag-over");
                    });
                    li.addEventListener("dragleave", function(){
                        li.classList.remove("is-drag-over");
                    });
                    li.addEventListener("drop", function(e){
                        e.preventDefault();
                        li.classList.remove("is-drag-over");
                        var dragging = list.querySelector(".aieo-pd-order-item.is-dragging");
                        if (!dragging || dragging === li) { return; }
                        // Insert dragging element before or after the drop target
                        // based on cursor Y within the target rect.
                        var rect = li.getBoundingClientRect();
                        var above = (e.clientY - rect.top) < (rect.height / 2);
                        list.insertBefore(dragging, above ? li : li.nextSibling);
                        saveOrder();
                    });
                });
                // Zone dropdowns — save on change (also persists the order).
                list.querySelectorAll(".aieo-pd-zone").forEach(function(sel){
                    sel.addEventListener("change", saveOrder);
                    sel.addEventListener("mousedown", function(e){ e.stopPropagation(); });
                });
            }
            function saveOrder(){
                if (!list) return;
                var ids = Array.prototype.map.call(
                    list.querySelectorAll(".aieo-pd-order-item"),
                    function(n){ return n.dataset.id; }
                );
                var fd = new FormData();
                fd.set("action", "aieo_dmm_product_display_save_order");
                fd.set("nonce",  "' . esc_js($nonce) . '");
                ids.forEach(function(id){ fd.append("order[]", id); });
                // Per-item placement zone — POST the raw value (info / under /
                // beside_title); ajax_save_order() validates per element.
                list.querySelectorAll(".aieo-pd-zone").forEach(function(sel){
                    fd.append("zones[" + sel.dataset.id + "]", sel.value);
                });
                orderStat.style.color = "#646970";
                orderStat.textContent = "' . esc_js(__('Saving order…', 'ai-eshop-optimizer')) . '";
                fetch("' . esc_js($ajax) . '", {method:"POST", credentials:"same-origin", body:fd})
                    .then(function(r){return r.json();})
                    .then(function(j){
                        if (j && j.success) {
                            orderStat.style.color = "#15803d";
                            orderStat.textContent = j.data.message;
                        } else {
                            orderStat.style.color = "#b91c1c";
                            orderStat.textContent = (j && j.data && j.data.message) || "' . esc_js(__('Save failed.', 'ai-eshop-optimizer')) . '";
                        }
                    });
            }

            // ── Recommendations Ordering — drag + visibility ──────────
            // Mirrors the cluster drag-and-drop above, but the saved
            // payload also carries a `visible[id]=1|0` map populated
            // from each row\'s Show checkbox.
            var rList     = document.getElementById("aieo-reco-order-list");
            var rOrderStat= document.getElementById("aieo-reco-order-status");
            if (rList) {
                rList.querySelectorAll(".aieo-reco-order-item").forEach(function(li){
                    li.setAttribute("draggable", "true");
                    li.addEventListener("dragstart", function(e){
                        li.classList.add("is-dragging");
                        e.dataTransfer.effectAllowed = "move";
                        e.dataTransfer.setData("text/plain", li.dataset.id);
                    });
                    li.addEventListener("dragend", function(){
                        li.classList.remove("is-dragging");
                        rList.querySelectorAll(".aieo-reco-order-item").forEach(function(n){ n.classList.remove("is-drag-over"); });
                    });
                    li.addEventListener("dragover", function(e){
                        e.preventDefault();
                        e.dataTransfer.dropEffect = "move";
                        li.classList.add("is-drag-over");
                    });
                    li.addEventListener("dragleave", function(){
                        li.classList.remove("is-drag-over");
                    });
                    li.addEventListener("drop", function(e){
                        e.preventDefault();
                        li.classList.remove("is-drag-over");
                        var dragging = rList.querySelector(".aieo-reco-order-item.is-dragging");
                        if (!dragging || dragging === li) { return; }
                        var rect = li.getBoundingClientRect();
                        var above = (e.clientY - rect.top) < (rect.height / 2);
                        rList.insertBefore(dragging, above ? li : li.nextSibling);
                        saveRecoOrder();
                    });
                });
                rList.querySelectorAll("[data-aieo-reco-visible]").forEach(function(cb){
                    cb.addEventListener("change", saveRecoOrder);
                });
            }
            function saveRecoOrder(){
                if (!rList) return;
                var items = rList.querySelectorAll(".aieo-reco-order-item");
                var fd = new FormData();
                fd.set("action", "aieo_dmm_recommendations_save_order");
                fd.set("nonce",  "' . esc_js($nonce) . '");
                items.forEach(function(li){
                    var id = li.dataset.id;
                    fd.append("order[]", id);
                    var cb = li.querySelector("[data-aieo-reco-visible]");
                    fd.set("visible[" + id + "]", (cb && cb.checked) ? "1" : "0");
                });
                rOrderStat.style.color = "#646970";
                rOrderStat.textContent = "' . esc_js(__('Saving order…', 'ai-eshop-optimizer')) . '";
                fetch("' . esc_js($ajax) . '", {method:"POST", credentials:"same-origin", body:fd})
                    .then(function(r){return r.json();})
                    .then(function(j){
                        if (j && j.success) {
                            rOrderStat.style.color = "#15803d";
                            rOrderStat.textContent = j.data.message;
                        } else {
                            rOrderStat.style.color = "#b91c1c";
                            rOrderStat.textContent = (j && j.data && j.data.message) || "' . esc_js(__('Save failed.', 'ai-eshop-optimizer')) . '";
                        }
                    });
            }
        })();</script>';

        echo '</div>';
    }
}
