<?php

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

class AIEO_DMM_Navigator extends AIEO_DMM_Module_Base {

    public function get_id()       { return 'navigator'; }
    public function get_label()    { return __('Navigator', 'ai-eshop-optimizer'); }
    public function get_priority() { return 70; }
    public function get_group()    { return 'visuals'; }

    
    private $hooked = false;

    
    
    

    public function register_hooks() {
        
        add_action('wp_ajax_aieo_dmm_navigator_save_settings', array($this, 'ajax_save_settings'));
        add_action('wp_ajax_aieo_dmm_navigator_regenerate',    array($this, 'ajax_regenerate'));

        
        
        
        add_action('aieo_dmm_navigator_cron_regen', array($this, 'cron_regenerate'));

        if (!$this->is_enabled()) {
            return;
        }
        if ($this->hooked) {
            return;
        }
        $this->hooked = true;

        
        
        $this->ensure_cron_scheduled();

        
        
        add_action('wp_enqueue_scripts', array($this, 'enqueue_assets'));

        
        
        
        
        
        
        add_action('woocommerce_product_query', array($this, 'scope_need_archive_to_category'));

        
        
        
        
        
        add_filter('query_loop_block_query_vars', array($this, 'scope_product_collection_query'), 9, 3);

        
        
        
        add_filter('wp_robots', array($this, 'noindex_scoped_need_archive'));

        
        
        add_action('wp_footer', array($this, 'render_modal_shell'), 90);

        
        
        
        
        
        

        
        
        
        
        
        

        
        add_action('save_post_page',     array($this, 'mark_dirty'), 99);
        add_action('save_post_post',     array($this, 'mark_dirty'), 99);
        add_action('save_post_product',  array($this, 'mark_dirty'), 99);
        add_action('wp_trash_post',      array($this, 'mark_dirty'), 99);
        add_action('untrash_post',       array($this, 'mark_dirty'), 99);
        add_action('deleted_post',       array($this, 'mark_dirty'), 99);

        
        add_action('created_term',       array($this, 'on_term_change'), 99, 3);
        add_action('edited_term',        array($this, 'on_term_change'), 99, 3);
        add_action('delete_term',        array($this, 'on_term_change'), 99, 3);
        add_action('set_object_terms',   array($this, 'on_set_object_terms'), 99, 6);

        
        add_action('wp_update_nav_menu', array($this, 'mark_dirty'), 99);
        add_action('wp_create_nav_menu', array($this, 'mark_dirty'), 99);
        add_action('wp_delete_nav_menu', array($this, 'mark_dirty'), 99);

        
        add_action('shutdown', array($this, 'maybe_regenerate'), 5);

        
        
        
        
        
        

        
        
        
        add_filter('rocket_cache_reject_uri',                  array($this, 'wp_rocket_exclude'));
        add_filter('rocket_cdn_reject_files',                  array($this, 'wp_rocket_exclude'));
        
        
        add_filter('rocket_exclude_js',                         array($this, 'wp_rocket_exclude_js'));
        add_filter('rocket_minify_excluded_external_paths',     array($this, 'wp_rocket_exclude_js'));

        
        
        
        
        
        add_action('init', array($this, 'send_nocache_headers_for_json'));
    }

    
    public function wp_rocket_exclude($paths) {
        if (!is_array($paths)) { $paths = array(); }
        $paths[] = '/wp-content/uploads/aieo/navigator.json';
        $paths[] = '(.*)/aieo/navigator\.json(.*)';
        return $paths;
    }

    
    public function wp_rocket_exclude_js($paths) {
        if (!is_array($paths)) { $paths = array(); }
        $paths[] = '/wp-content/plugins/ai-eshop-optimizer/assets/js/aieo-navigator.js';
        return $paths;
    }

    
    public function send_nocache_headers_for_json() {
        if (!isset($_SERVER['REQUEST_URI'])) { return; }
        $uri = (string) sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
        if (strpos($uri, '/wp-content/uploads/aieo/navigator.json') === false) { return; }
        if (headers_sent()) { return; }
        nocache_headers();
        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0', true);
        header('Pragma: no-cache', true);
        header('Expires: Thu, 01 Jan 1970 00:00:00 GMT', true);
    }

    
    public function scope_need_archive_to_category($q) {
        if (is_admin() || !is_object($q) || !method_exists($q, 'is_main_query') || !$q->is_main_query()) {
            return;
        }
        if (!$this->needs_scoped_enabled()) {
            return;
        }
        
        
        
        
        if (!method_exists($q, 'is_tax') || !$q->is_tax(array('pa_need', 'pa-need', 'product_brand', 'pwb-brand'))) {
            return;
        }
        
        if ($q->get('post__in')) {
            return;
        }
        if (function_exists('doing_action') && doing_action('dynamic_sidebar')) {
            return;
        }
        
        $cat_id = isset($_GET['in_cat']) ? absint(wp_unslash($_GET['in_cat'])) : 0;
        if ($cat_id <= 0) {
            return;
        }
        $term = get_term($cat_id, 'product_cat');
        if (!$term || is_wp_error($term)) {
            return;
        }
        $tax_query = $q->get('tax_query');
        if (!is_array($tax_query)) {
            $tax_query = array();
        }
        
        
        $tax_query[] = array(
            'taxonomy'         => 'product_cat',
            'field'            => 'term_id',
            'terms'            => array($cat_id),
            'include_children' => true,
        );
        $q->set('tax_query', $tax_query);
    }

    
    public function scope_product_collection_query($query, $block, $page) {
        if (is_admin() || !$this->needs_scoped_enabled()) {
            return $query;
        }
        if (!is_object($block) || empty($block->context['query']['inherit'])) {
            return $query;
        }
        if (!function_exists('is_tax') || !is_tax(array('pa_need', 'pa-need', 'product_brand', 'pwb-brand'))) {
            return $query;
        }
        
        $cat_id = isset($_GET['in_cat']) ? absint(wp_unslash($_GET['in_cat'])) : 0;
        if ($cat_id <= 0) {
            return $query;
        }
        $term = get_term($cat_id, 'product_cat');
        if (!$term || is_wp_error($term)) {
            return $query;
        }
        $tax_query = (isset($query['tax_query']) && is_array($query['tax_query'])) ? $query['tax_query'] : array();
        $tax_query[] = array(
            'taxonomy'         => 'product_cat',
            'field'            => 'term_id',
            'terms'            => array($cat_id),
            'include_children' => true,
        );
        $query['tax_query'] = $tax_query;
        return $query;
    }

    
    public function noindex_scoped_need_archive($robots) {
        if (is_admin() || !$this->needs_scoped_enabled()) {
            return $robots;
        }
        
        if (empty($_GET['in_cat'])) {
            return $robots;
        }
        if (function_exists('is_tax') && is_tax(array('pa_need', 'pa-need', 'product_brand', 'pwb-brand'))) {
            $robots['noindex'] = true;
            $robots['follow']  = true;
        }
        return $robots;
    }

    
    
    

    
    private function excluded_term_keys() {
        $raw = (string) AIEO_DMM_Settings::get('navigator_excluded_terms', '');
        
        
        
        
        
        
        
        $aliases = array(
            'product_brand' => array('product_brand', 'pwb-brand', 'pa_brand', 'pa-brand'),
            'pwb-brand'     => array('product_brand', 'pwb-brand', 'pa_brand', 'pa-brand'),
            'pa_brand'      => array('product_brand', 'pwb-brand', 'pa_brand', 'pa-brand'),
            'pa-brand'      => array('product_brand', 'pwb-brand', 'pa_brand', 'pa-brand'),
            'pa-need'       => array('pa-need', 'pa_need'),
            'pa_need'       => array('pa-need', 'pa_need'),
        );
        $out = array();
        foreach (preg_split('/[,\s]+/', $raw) as $tok) {
            $tok = trim($tok);
            if ($tok === '') continue;
            if (strpos($tok, ':') === false) {
                $out[] = 'product_cat:' . (int) $tok;
                continue;
            }
            list($tax, $id) = explode(':', $tok, 2);
            $tax = sanitize_key($tax);
            $id  = (int) $id;
            $taxes = isset($aliases[$tax]) ? $aliases[$tax] : array($tax);
            foreach ($taxes as $alias) {
                $out[] = $alias . ':' . $id;
            }
        }
        return array_flip(array_unique($out));
    }

    
    public function is_enabled() {
        if (method_exists('AIEO_DMM_Settings', 'is_module_enabled')) {
            return (bool) AIEO_DMM_Settings::is_module_enabled('navigator');
        }
        return (bool) AIEO_DMM_Settings::get('navigator_enabled', true);
    }

    
    private function needs_scoped_enabled() {
        return (bool) AIEO_DMM_Settings::get('navigator_needs_scoped', false);
    }

    
    public static function group_catalog() {
        return array(
            
            
            
            
            
            
            
            array('slug' => 'filters',         'label' => __('Φίλτρα (FiboFilters)', 'ai-eshop-optimizer'), 'default_placement' => 'main'),
            array('slug' => 'categories',      'label' => __('Κατηγορίες', 'ai-eshop-optimizer'),       'default_placement' => 'main'),
            array('slug' => 'needs',           'label' => __('Επιθυμίες', 'ai-eshop-optimizer'),          'default_placement' => 'main'),
            array('slug' => 'brands',          'label' => __('Brands', 'ai-eshop-optimizer'),           'default_placement' => 'main'),
            array('slug' => 'pages',           'label' => __('Σελίδες', 'ai-eshop-optimizer'),          'default_placement' => 'main'),
            array('slug' => 'blog-categories', 'label' => __('Κατηγορίες Blog', 'ai-eshop-optimizer'),  'default_placement' => 'main'),
            array('slug' => 'blog-posts',      'label' => __('Blog', 'ai-eshop-optimizer'),             'default_placement' => 'main'),
            array('slug' => 'actions',         'label' => __('Εργαλεία', 'ai-eshop-optimizer'),         'default_placement' => 'rail'),
        );
    }

    
    private function resolve_groups_config() {
        $catalog = self::group_catalog();
        $by_slug = array();
        foreach ($catalog as $g) { $by_slug[$g['slug']] = $g; }

        $saved = AIEO_DMM_Settings::get('navigator_groups_config', null);
        if (!is_array($saved)) { $saved = array(); }

        $resolved = array();
        $seen = array();
        foreach ($saved as $row) {
            if (!is_array($row) || empty($row['slug'])) { continue; }
            $slug = sanitize_key($row['slug']);
            if (!isset($by_slug[$slug]) || isset($seen[$slug])) { continue; }
            $place = isset($row['placement']) ? sanitize_key($row['placement']) : $by_slug[$slug]['default_placement'];
            if (!in_array($place, array('main', 'rail', 'off'), true)) {
                $place = $by_slug[$slug]['default_placement'];
            }
            $resolved[] = array('slug' => $slug, 'placement' => $place);
            $seen[$slug] = true;
        }
        
        foreach ($catalog as $g) {
            if (isset($seen[$g['slug']])) { continue; }
            $resolved[] = array('slug' => $g['slug'], 'placement' => $g['default_placement']);
        }
        return $resolved;
    }

    
    private function default_actions() {
        return apply_filters('aieo_dmm_navigator_actions', array(
            
            
            
            
            array('id'=>'act-wishlist',  'label'=>__('Λίστα επιθυμιών', 'ai-eshop-optimizer'),  'url'=>'/wishlist/',         'icon'=>'heart', 'event'=>'aieo:side-cart:open:favorites'),
            array('id'=>'act-orders',    'label'=>__('Οι παραγγελίες μου', 'ai-eshop-optimizer'), 'url'=>'/my-account/orders/', 'icon'=>'package'),
            array('id'=>'act-account',   'label'=>__('Ο λογαριασμός μου', 'ai-eshop-optimizer'),  'url'=>'/my-account/',       'icon'=>'user'),
            array('id'=>'act-cart',      'label'=>__('Καλάθι', 'ai-eshop-optimizer'),            'url'=>'/cart/',             'icon'=>'shopping-bag', 'event'=>'aieo:side-cart:open:cart'),
            array('id'=>'act-rv',        'label'=>__('Πρόσφατα προϊόντα', 'ai-eshop-optimizer'),  'url'=>'#',                  'icon'=>'clock', 'event'=>'aieo:side-cart:open:recently-viewed'),
            array('id'=>'act-fastfinder', 'label'=>__('Fast Finder', 'ai-eshop-optimizer'),           'url'=>'/fast-finder/',             'icon'=>'search'),
            array('id'=>'act-brands',     'label'=>__('Brands', 'ai-eshop-optimizer'),                'url'=>'/brand-shopping/',          'icon'=>'tag'),
            array('id'=>'act-competitions', 'label'=>__('Διαγωνισμοί', 'ai-eshop-optimizer'),         'url'=>'/competitions/',            'icon'=>'zap'),
            array('id'=>'act-pros',       'label'=>__('Εγγραφή Επαγγελματιών', 'ai-eshop-optimizer'), 'url'=>'/epaggelmaties-spoudastes/', 'icon'=>'user'),
            array('id'=>'act-stores',    'label'=>__('Καταστήματα', 'ai-eshop-optimizer'),       'url'=>'/katastimata/',      'icon'=>'map-pin'),
            array('id'=>'act-contact',   'label'=>__('Επικοινωνία', 'ai-eshop-optimizer'),       'url'=>'/contact/',          'icon'=>'mail'),
            array('id'=>'act-faq',       'label'=>__('Συχνές Ερωτήσεις', 'ai-eshop-optimizer'),  'url'=>'/faq/',              'icon'=>'help-circle'),
        ));
    }

    
    
    

    
    public function mark_dirty() {
        
        
        static $marked = false;
        if ($marked) return;
        set_transient('aieo_dmm_navigator_dirty', 1, HOUR_IN_SECONDS);
        $marked = true;
    }

    
    public function on_term_change($term_id, $tt_id = 0, $taxonomy = '') {
        $watched = array('product_cat', 'product_brand', 'pa-need', 'category');
        if (in_array($taxonomy, $watched, true)) {
            $this->mark_dirty();
        }
    }

    
    public function on_set_object_terms($object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids) {
        $watched = array('product_cat', 'product_brand', 'pa-need', 'category');
        if (in_array($taxonomy, $watched, true)) {
            $this->mark_dirty();
        }
    }

    
    public function maybe_regenerate() {
        if (!get_transient('aieo_dmm_navigator_dirty')) {
            return;
        }
        delete_transient('aieo_dmm_navigator_dirty');
        $this->generate_json();
    }

    
    
    

    const CRON_HOOK = 'aieo_dmm_navigator_cron_regen';

    
    private function daily_time() {
        $t = (string) AIEO_DMM_Settings::get('navigator_daily_time', '03:00');
        
        if (!preg_match('/^([01]\d|2[0-3]):[0-5]\d$/', $t)) {
            $t = '03:00';
        }
        return $t;
    }

    
    private function daily_enabled() {
        return (bool) AIEO_DMM_Settings::get('navigator_daily_enabled', true);
    }

    
    private function next_run_timestamp($time_hhmm) {
        $tz = function_exists('wp_timezone') ? wp_timezone() : new DateTimeZone(date_default_timezone_get());
        try {
            $dt = new DateTime('today ' . $time_hhmm, $tz);
        } catch (Exception $e) {
            $dt = new DateTime('today 03:00', $tz);
        }
        if ($dt->getTimestamp() <= time()) {
            $dt->modify('+1 day');
        }
        return $dt->getTimestamp();
    }

    
    public function ensure_cron_scheduled() {
        if (!$this->daily_enabled()) {
            wp_clear_scheduled_hook(self::CRON_HOOK);
            return;
        }
        $target = $this->next_run_timestamp($this->daily_time());
        $existing = wp_next_scheduled(self::CRON_HOOK);
        
        
        
        if (!$existing || abs($existing - $target) > 300) {
            wp_clear_scheduled_hook(self::CRON_HOOK);
            wp_schedule_event($target, 'daily', self::CRON_HOOK);
        }
    }

    
    public function cron_regenerate() {
        if (!$this->is_enabled()) return;
        $this->generate_json();
    }

    
    
    

    
    public function generate_json($force = false) {
        $upload = wp_upload_dir();
        $dir = trailingslashit($upload['basedir']) . 'aieo';
        if (!file_exists($dir)) {
            wp_mkdir_p($dir);
        }

        $version = $this->next_version();

        
        
        
        $builders = array(
            'categories'      => array($this, 'build_categories_group'),
            'needs'           => array($this, 'build_needs_group'),
            'brands'          => array($this, 'build_brands_group'),
            'pages'           => array($this, 'build_pages_group'),
            'blog-categories' => array($this, 'build_blog_categories_group'),
            'blog-posts'      => array($this, 'build_blog_posts_group'),
            'actions'         => array($this, 'build_actions_group'),
        );

        $groups = array();
        foreach ($this->resolve_groups_config() as $cfg) {
            if ($cfg['placement'] === 'off') { continue; }
            if (empty($builders[$cfg['slug']])) { continue; }
            $g = call_user_func($builders[$cfg['slug']]);
            if (empty($g['items'])) { continue; }      
            $g['placement'] = $cfg['placement'];        
            $groups[] = $g;
        }

        
        
        
        
        
        
        
        $built_slugs = array_flip(array_column($groups, 'slug'));
        $spine = array('categories' => true, 'brands' => true);
        $missing_spine = array();
        foreach ($spine as $slug => $_) {
            if (empty($builders[$slug])) { continue; }   
            if (isset($built_slugs[$slug])) { continue; } 
            
            
            
            $turned_off = false;
            foreach ($this->resolve_groups_config() as $cfg) {
                if ($cfg['slug'] === $slug && $cfg['placement'] === 'off') { $turned_off = true; break; }
            }
            if (!$turned_off) { $missing_spine[] = $slug; }
        }
        if (!empty($missing_spine) && !$force && file_exists($dir . '/navigator.json')) {
            
            
            
            
            
            
            set_transient('aieo_dmm_navigator_last_abort', array(
                'at'      => time(),
                'missing' => $missing_spine,
            ), HOUR_IN_SECONDS);
            return false;
        }

        $payload = array(
            'version'       => $version,
            'generated_at'  => time(),
            'site_url'      => untrailingslashit(home_url('/')),
            'groups'        => $groups,
            
            
            
            'highlights'    => array(
                'brands' => $this->top_brands(10),
                'needs'  => $this->top_needs(10),
            ),
        );

        
        
        
        
        
        
        if ($this->needs_scoped_enabled()) {
            $scope_maps = $this->build_category_scope_maps();
            $payload['needsByCategory']  = $scope_maps['needs'];
            $payload['brandsByCategory'] = $scope_maps['brands'];
        }

        $path = $dir . '/navigator.json';
        $json = wp_json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        if ($json === false) {
            return false;
        }
        $ok = (false !== @file_put_contents($path, $json));

        if ($ok) {
            update_option('aieo_dmm_navigator_version', $version, true);
            update_option('aieo_dmm_navigator_generated_at', time(), true);
        }
        return $ok;
    }

    
    private function next_version() {
        $current = (string) get_option('aieo_dmm_navigator_version', '1.0.0');
        $parts = explode('.', $current);
        $parts[2] = isset($parts[2]) ? ((int) $parts[2] + 1) : 1;
        return implode('.', $parts);
    }

    
    
    

    
    private function build_categories_group() {
        $items = $this->terms_to_items('product_cat', 3);
        return array(
            'slug'  => 'categories',
            'label' => __('Κατηγορίες', 'ai-eshop-optimizer'),
            'icon'  => 'grid',
            'items' => $items,
        );
    }

    
    private function build_brands_group() {
        $items = $this->popularity_brands();
        if (empty($items)) {
            $tax = taxonomy_exists('product_brand') ? 'product_brand'
                 : (taxonomy_exists('pwb-brand') ? 'pwb-brand' : null);
            $items = $tax ? $this->terms_to_items($tax, 1) : array();
        }
        
        
        
        
        
        
        
        return array(
            'slug'  => 'brands',
            'label' => __('Brands', 'ai-eshop-optimizer'),
            'icon'  => 'tag',
            'items' => $items,
        );
    }

    
    private function popularity_table() {
        global $wpdb;
        return get_option('aieo_core_category_brand_popularity_table', $wpdb->prefix . 'aieo_core_category_brand_popularity');
    }

    
    private function popularity_table_exists() {
        global $wpdb;
        $tbl = $this->popularity_table();
        
        $found = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $tbl));
        return ($found === $tbl);
    }

    
    private function popularity_categories() {
        if (!$this->popularity_table_exists()) {
            return array();
        }
        $this->build_term_url_map('product_cat');
        global $wpdb;
        $tbl = $this->popularity_table();
        
        $order_by = (string) AIEO_DMM_Settings::get('navigator_order_metric', 'turnover');
        $col      = ($order_by === 'units') ? 'total_sales' : 'total_turnover';
        
        $rows = $wpdb->get_results(
            "SELECT category_id, category_name, category_level, parent_category_id, product_count, total_sales, total_turnover
             FROM {$tbl}
             WHERE category_id IS NOT NULL AND (brand_id IS NULL OR brand_id = 0)
               AND product_count > 0
             ORDER BY category_level ASC, {$col} DESC"
        );
        if (empty($rows)) {
            return array();
        }
        $excluded = $this->excluded_term_keys();
        $items = array();
        foreach ($rows as $r) {
            
            
            
            if (isset($excluded['product_cat:' . (int) $r->category_id])) {
                continue;
            }
            $term = get_term((int) $r->category_id, 'product_cat');
            if (!$term || is_wp_error($term)) {
                continue; 
            }
            $url = $this->resolve_term_url($term, 'product_cat');
            $level = max(0, (int) $r->category_level - 1); 
            $parent_key = ($r->parent_category_id) ? ('product_cat-' . (int) $r->parent_category_id) : null;
            $items[] = array(
                'id'     => 'product_cat-' . (int) $term->term_id,
                'label'  => $r->category_name ?: $term->name,
                'url'    => $url,
                'level'  => $level,
                'parent' => $parent_key,
                'count'  => (int) $r->product_count,
            );
        }
        return $items;
    }

    
    
    private function resolve_term_url($term, $taxonomy = '') {
        if (!$term || is_wp_error($term)) {
            return '';
        }
        $term_id = is_object($term) ? (int) $term->term_id : 0;
        $tax     = $taxonomy ?: (is_object($term) ? (string) $term->taxonomy : '');
        if ($term_id > 0 && $tax !== '' && isset($this->term_url_maps[$tax][$term_id])) {
            return (string) $this->term_url_maps[$tax][$term_id];
        }
        
        
        
        $url = get_term_link($term);
        if (!is_wp_error($url) && is_string($url) && $url !== '') {
            return $url;
        }
        $slug = is_object($term) ? (string) $term->slug : '';
        if ($slug === '') { return ''; }
        switch ($tax) {
            case 'product_cat':
                return home_url('/product-category/' . rawurlencode($slug) . '/');
            case 'product_brand':
            case 'pwb-brand':
                return home_url('/brand/' . rawurlencode($slug) . '/');
            case 'pa-need':
            case 'pa_need':
                return home_url('/need/' . rawurlencode($slug) . '/');
            case 'category':
                return home_url('/category/' . rawurlencode($slug) . '/');
            default:
                return $tax
                    ? home_url('/' . rawurlencode($tax) . '/' . rawurlencode($slug) . '/')
                    : '';
        }
    }

    
    private $term_url_maps = array();

    
    private function build_term_url_map($taxonomy) {
        if (isset($this->term_url_maps[$taxonomy])) {
            return $this->term_url_maps[$taxonomy];
        }
        $this->term_url_maps[$taxonomy] = array();
        if (!taxonomy_exists($taxonomy)) { return $this->term_url_maps[$taxonomy]; }

        global $wpdb;
        
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT t.term_id, t.slug, tt.parent
             FROM {$wpdb->terms} t
             INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_id = t.term_id
             WHERE tt.taxonomy = %s",
            $taxonomy
        ));
        if (empty($rows)) { return $this->term_url_maps[$taxonomy]; }

        
        $slug_by_id   = array();
        $parent_by_id = array();
        foreach ($rows as $r) {
            $tid = (int) $r->term_id;
            $slug_by_id[$tid]   = (string) $r->slug;
            $parent_by_id[$tid] = (int) $r->parent;
        }

        $base = $this->taxonomy_url_base($taxonomy);
        $hierarchical = in_array($taxonomy, array('product_cat', 'category'), true);

        foreach ($slug_by_id as $tid => $slug) {
            if ($slug === '') { $this->term_url_maps[$taxonomy][$tid] = ''; continue; }
            if (!$hierarchical) {
                $this->term_url_maps[$taxonomy][$tid] = home_url($base . '/' . rawurlencode($slug) . '/');
                continue;
            }
            
            
            $chain   = array($slug);
            $cursor  = $parent_by_id[$tid];
            $guard   = 0;
            while ($cursor > 0 && $guard < 32) {
                if (!isset($slug_by_id[$cursor])) { break; }
                array_unshift($chain, $slug_by_id[$cursor]);
                $cursor = isset($parent_by_id[$cursor]) ? $parent_by_id[$cursor] : 0;
                $guard++;
            }
            $path = implode('/', array_map('rawurlencode', $chain));
            $this->term_url_maps[$taxonomy][$tid] = home_url($base . '/' . $path . '/');
        }
        return $this->term_url_maps[$taxonomy];
    }

    
    private function taxonomy_url_base($taxonomy) {
        if ($taxonomy === 'product_cat') {
            $perm = get_option('woocommerce_permalinks', array());
            $base = is_array($perm) && !empty($perm['category_base']) ? (string) $perm['category_base'] : 'product-category';
            return '/' . trim($base, '/');
        }
        if ($taxonomy === 'product_brand' || $taxonomy === 'pwb-brand') {
            return '/brand';
        }
        if ($taxonomy === 'pa-need' || $taxonomy === 'pa_need') {
            return '/need';
        }
        if ($taxonomy === 'category') { return '/category'; }
        return '/' . trim($taxonomy, '/');
    }

    
    private function popularity_brands() {
        if (!$this->popularity_table_exists()) {
            return array();
        }
        $tax = taxonomy_exists('product_brand') ? 'product_brand'
             : (taxonomy_exists('pwb-brand') ? 'pwb-brand' : null);
        if ($tax) { $this->build_term_url_map($tax); }
        global $wpdb;
        $tbl   = $this->popularity_table();
        $extra = $wpdb->prefix . 'aieo_dmm_brand_extras';
        $order_by = (string) AIEO_DMM_Settings::get('navigator_order_metric', 'turnover');
        $col      = ($order_by === 'units') ? 'total_sales' : 'total_turnover';
        
        
        
        
        $rows = $wpdb->get_results(
            "SELECT pop.brand_id, pop.brand_name, pop.brand_slug, pop.product_count,
                    pop.total_sales, pop.total_turnover,
                    be.logo_image_id, be.hero_image_id
               FROM {$tbl} pop
          LEFT JOIN {$extra} be ON be.term_id = pop.brand_id AND be.taxonomy = 'product_brand'
              WHERE (pop.category_id IS NULL OR pop.category_id = 0)
                AND pop.brand_id IS NOT NULL AND pop.brand_id > 0
                AND pop.product_count > 0
              ORDER BY pop.{$col} DESC"
        );
        if (empty($rows)) {
            return array();
        }
        $tax = taxonomy_exists('product_brand') ? 'product_brand'
             : (taxonomy_exists('pwb-brand') ? 'pwb-brand' : null);

        $excluded = $this->excluded_term_keys();
        $items = array();
        foreach ($rows as $r) {
            $bid = (int) $r->brand_id;
            if ($tax && (
                isset($excluded[$tax . ':' . $bid]) ||
                isset($excluded['product_brand:' . $bid]) ||
                isset($excluded['pwb-brand:' . $bid])
            )) {
                continue;
            }
            $url = '';
            $term = null;
            if ($tax) {
                $term = get_term($bid, $tax);
                if ($term && !is_wp_error($term)) {
                    $url = $this->resolve_term_url($term, $tax);
                }
            }
            if (!$url && !empty($r->brand_slug)) {
                $url = home_url('/brand/' . rawurlencode($r->brand_slug) . '/');
            }
            
            
            
            $logo_url = '';
            if (!empty($r->logo_image_id)) {
                $logo_url = (string) wp_get_attachment_image_url((int) $r->logo_image_id, 'medium');
            }
            if (!$logo_url) {
                $thumb_id = (int) get_term_meta($bid, 'thumbnail_id', true);
                if ($thumb_id > 0) {
                    $logo_url = (string) wp_get_attachment_image_url($thumb_id, 'medium');
                }
            }
            $item = array(
                'id'    => 'brand-' . $bid,
                'label' => $r->brand_name,
                'url'   => $url,
                'level' => 0,
                'count' => (int) $r->product_count,
            );
            if ($logo_url !== '') { $item['image'] = $logo_url; }
            $items[] = $item;
        }
        return $items;
    }

    
    private function favourite_brands_for_viewer($limit = 5) {
        if (!is_user_logged_in()) { return array(); }
        if (!class_exists('AIEO_Customer_Intelligence_Query')) { return array(); }

        $uid = get_current_user_id();
        $top_ids = AIEO_Customer_Intelligence_Query::get_top_brands($uid, $limit);
        if (empty($top_ids)) { return array(); }

        
        
        
        $all = $this->popularity_brands();
        $by_id = array();
        foreach ($all as $it) {
            if (preg_match('/^brand-(\d+)$/', (string) $it['id'], $m)) {
                $by_id[(int) $m[1]] = $it;
            }
        }
        $out = array();
        foreach ($top_ids as $bid) {
            $bid = (int) $bid;
            if (!isset($by_id[$bid])) { continue; }
            $it = $by_id[$bid];
            $it['id'] = 'fav-brand-' . $bid;
            $it['favourite'] = 1;
            $out[] = $it;
        }
        return $out;
    }

    
    private function top_brands($limit = 10) {
        $items = $this->popularity_brands();
        if (empty($items)) {
            $tax = taxonomy_exists('product_brand') ? 'product_brand'
                 : (taxonomy_exists('pwb-brand') ? 'pwb-brand' : null);
            $items = $tax ? $this->terms_to_items($tax, 1) : array();
        }
        return array_slice($items, 0, max(0, (int) $limit));
    }

    
    private function top_needs($limit = 10) {
        $tax = taxonomy_exists('pa-need') ? 'pa-need'
             : (taxonomy_exists('pa_need') ? 'pa_need' : null);
        if (!$tax) {
            return array();
        }
        $terms = get_terms(array(
            'taxonomy'   => $tax,
            'hide_empty' => true,
            'orderby'    => 'count',
            'order'      => 'DESC',
            'number'     => max(0, (int) $limit),
        ));
        if (is_wp_error($terms) || empty($terms)) {
            return array();
        }
        $excluded = $this->excluded_term_keys();
        $items = array();
        foreach ($terms as $t) {
            if (isset($excluded[$tax . ':' . (int) $t->term_id])) continue;
            $items[] = array(
                'id'    => $tax . '-' . (int) $t->term_id,
                'label' => $t->name,
                'url'   => get_term_link($t),
                'level' => 0,
                'count' => (int) $t->count,
            );
        }
        return $items;
    }

    private function build_needs_group() {
        $tax = taxonomy_exists('pa-need') ? 'pa-need'
             : (taxonomy_exists('pa_need') ? 'pa_need' : null);
        if (!$tax) {
            return array('slug' => 'needs', 'label' => __('Επιθυμίες', 'ai-eshop-optimizer'), 'items' => array());
        }
        return array(
            'slug'  => 'needs',
            'label' => __('Επιθυμίες', 'ai-eshop-optimizer'),
            'icon'  => 'sparkles',
            'items' => $this->terms_to_items($tax, 1),
        );
    }

    
    private function build_term_by_category_map($child_taxonomy) {
        global $wpdb;
        if (!$child_taxonomy || !taxonomy_exists($child_taxonomy)) {
            return new stdClass();
        }

        
        
        $pairs = $wpdb->get_results($wpdb->prepare(
            "SELECT ttc.term_id AS cat_id, ttx.term_id AS child_id, COUNT(*) AS cnt
               FROM {$wpdb->term_relationships} trc
         INNER JOIN {$wpdb->term_taxonomy} ttc
                 ON ttc.term_taxonomy_id = trc.term_taxonomy_id AND ttc.taxonomy = 'product_cat'
         INNER JOIN {$wpdb->posts} p
                 ON p.ID = trc.object_id AND p.post_type = 'product' AND p.post_status = 'publish'
         INNER JOIN {$wpdb->term_relationships} trx
                 ON trx.object_id = p.ID
         INNER JOIN {$wpdb->term_taxonomy} ttx
                 ON ttx.term_taxonomy_id = trx.term_taxonomy_id AND ttx.taxonomy = %s
           GROUP BY ttc.term_id, ttx.term_id",
            $child_taxonomy
        ));
        if (empty($pairs)) {
            return new stdClass();
        }
        $direct = array(); 
        foreach ($pairs as $r) {
            $cid = (int) $r->cat_id;
            $xid = (int) $r->child_id;
            if ($cid > 0 && $xid > 0) {
                $direct[$cid][$xid] = (int) $r->cnt;
            }
        }

        
        
        $cat_rows = $wpdb->get_results(
            "SELECT term_id, parent FROM {$wpdb->term_taxonomy} WHERE taxonomy = 'product_cat'"
        );
        $children = array(); 
        $cat_ids  = array();
        foreach ((array) $cat_rows as $cr) {
            $tid = (int) $cr->term_id;
            $pid = (int) $cr->parent;
            $children[$pid][] = $tid;
            $cat_ids[$tid] = true;
        }

        
        
        $memo = array();
        $resolve = function ($cat) use (&$resolve, &$memo, $children, $direct) {
            if (isset($memo[$cat])) {
                return $memo[$cat];
            }
            $memo[$cat] = array(); 
            $set = isset($direct[$cat]) ? $direct[$cat] : array();
            if (!empty($children[$cat])) {
                foreach ($children[$cat] as $kid) {
                    foreach ($resolve($kid) as $xid => $cnt) {
                        $set[$xid] = (isset($set[$xid]) ? $set[$xid] : 0) + $cnt;
                    }
                }
            }
            $memo[$cat] = $set;
            return $set;
        };

        $out = array();
        foreach (array_keys($cat_ids) as $cat) {
            $set = $resolve($cat);
            if (!empty($set)) {
                ksort($set); 
                $clean = array();
                foreach ($set as $xid => $cnt) {
                    $clean[(string) $xid] = (int) $cnt; 
                }
                $out[(string) $cat] = $clean;
            }
        }
        return empty($out) ? new stdClass() : $out;
    }

    
    private function build_category_scope_maps() {
        global $wpdb;
        $vitals = $wpdb->prefix . 'aieo_core_product_vitals';
        
        if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $vitals)) === $vitals) {
            $v = $this->build_scope_maps_from_vitals($vitals);
            if (is_array($v)) {
                return $v;
            }
        }
        
        
        $need_tax  = taxonomy_exists('pa_need') ? 'pa_need'
                   : (taxonomy_exists('pa-need') ? 'pa-need' : '');
        $brand_tax = taxonomy_exists('product_brand') ? 'product_brand'
                   : (taxonomy_exists('pwb-brand') ? 'pwb-brand' : '');
        return array(
            'needs'  => $this->build_term_by_category_map($need_tax),
            'brands' => $this->build_term_by_category_map($brand_tax),
        );
    }

    
    private function build_scope_maps_from_vitals($vitals) {
        global $wpdb;
        
        $cols = $wpdb->get_col("SHOW COLUMNS FROM {$vitals}");
        if (!in_array('category_ids', $cols, true) || !in_array('parent_id', $cols, true) || !in_array('status', $cols, true)) {
            return null;
        }
        $has_need  = in_array('primary_need_id', $cols, true);
        $has_brand = in_array('brand_id', $cols, true);
        if (!$has_need && !$has_brand) {
            return null;
        }

        
        
        $cat_rows = $wpdb->get_results("SELECT term_id, parent FROM {$wpdb->term_taxonomy} WHERE taxonomy = 'product_cat'");
        $parent = array();
        foreach ((array) $cat_rows as $cr) {
            $parent[(int) $cr->term_id] = (int) $cr->parent;
        }
        $anc_cache = array();
        $ancestors = function ($cat) use (&$anc_cache, $parent) {
            if (isset($anc_cache[$cat])) {
                return $anc_cache[$cat];
            }
            $chain = array($cat);
            $p = isset($parent[$cat]) ? $parent[$cat] : 0;
            $guard = 0;
            while ($p > 0 && $guard < 40) {
                $chain[] = $p;
                $p = isset($parent[$p]) ? $parent[$p] : 0;
                $guard++;
            }
            $anc_cache[$cat] = $chain;
            return $chain;
        };

        
        
        
        
        
        
        $hidden_slugs = array('exclude-from-catalog');
        if ('yes' === get_option('woocommerce_hide_out_of_stock_items')) {
            $hidden_slugs[] = 'outofstock';
        }
        $hidden = array();
        $ph = implode(',', array_fill(0, count($hidden_slugs), '%s'));
        
        $hidden_ids = $wpdb->get_col($wpdb->prepare(
            "SELECT DISTINCT tr.object_id
               FROM {$wpdb->term_relationships} tr
         INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
         INNER JOIN {$wpdb->terms} t ON t.term_id = tt.term_id
              WHERE tt.taxonomy = 'product_visibility' AND t.slug IN ({$ph})",
            ...$hidden_slugs
        ));
        foreach ((array) $hidden_ids as $hid) {
            $hidden[(int) $hid] = true;
        }

        
        
        
        
        
        
        $sel = 'vv.product_id, vv.category_ids';
        if ($has_brand) { $sel .= ', vv.brand_id'; }
        if ($has_need)  { $sel .= ', vv.primary_need_id, vv.secondary_need_id, vv.other_needs_ids'; }
        
        $rows = $wpdb->get_results(
            "SELECT {$sel}
               FROM {$vitals} vv
         INNER JOIN {$wpdb->posts} p ON p.ID = vv.product_id AND p.post_type = 'product' AND p.post_status = 'publish'
              WHERE vv.product_id = vv.parent_id"
        );
        if (empty($rows)) {
            return null;
        }

        $needs  = array();
        $brands = array();
        foreach ($rows as $r) {
            if (isset($hidden[(int) $r->product_id])) {
                continue; 
            }
            $cats = array();
            foreach (explode(',', (string) $r->category_ids) as $cid) {
                $cid = (int) $cid;
                if ($cid > 0) {
                    foreach ($ancestors($cid) as $a) { $cats[$a] = true; }
                }
            }
            if (empty($cats)) {
                continue;
            }
            $prod_needs = array();
            if ($has_need) {
                foreach (array($r->primary_need_id, $r->secondary_need_id) as $nid) {
                    $nid = (int) $nid;
                    if ($nid > 0) { $prod_needs[$nid] = true; }
                }
                foreach (explode(',', (string) $r->other_needs_ids) as $nid) {
                    $nid = (int) $nid;
                    if ($nid > 0) { $prod_needs[$nid] = true; }
                }
            }
            $bid = $has_brand ? (int) $r->brand_id : 0;
            foreach (array_keys($cats) as $cat) {
                foreach (array_keys($prod_needs) as $nid) {
                    $needs[$cat][$nid] = (isset($needs[$cat][$nid]) ? $needs[$cat][$nid] : 0) + 1;
                }
                if ($bid > 0) {
                    $brands[$cat][$bid] = (isset($brands[$cat][$bid]) ? $brands[$cat][$bid] : 0) + 1;
                }
            }
        }
        return array(
            'needs'  => $this->shape_scope_map($needs),
            'brands' => $this->shape_scope_map($brands),
        );
    }

    
    private function shape_scope_map($map) {
        $out = array();
        foreach ($map as $cat => $items) {
            if (empty($items)) {
                continue;
            }
            ksort($items);
            $clean = array();
            foreach ($items as $id => $cnt) {
                $clean[(string) $id] = (int) $cnt;
            }
            $out[(string) $cat] = $clean;
        }
        return empty($out) ? new stdClass() : $out;
    }

    private function build_pages_group() {
        return array(
            'slug'  => 'pages',
            'label' => __('Σελίδες', 'ai-eshop-optimizer'),
            'icon'  => 'file-text',
            'items' => $this->posts_to_items('page', 200, 'menu_order title', 'ASC'),
        );
    }

    private function build_blog_posts_group() {
        return array(
            'slug'  => 'blog-posts',
            'label' => __('Blog', 'ai-eshop-optimizer'),
            'icon'  => 'book-open',
            'items' => $this->posts_to_items('post', 100, 'date', 'DESC'),
        );
    }

    
    private function posts_to_items($post_type, $limit, $orderby, $order) {
        $ids = get_posts(array(
            'post_type'      => $post_type,
            'post_status'    => 'publish',
            'posts_per_page' => $limit,
            'orderby'        => $orderby,
            'order'          => $order,
            'no_found_rows'  => true,
            'fields'         => 'ids',
        ));
        $items = array();
        foreach ($ids as $pid) {
            $excerpt = get_the_excerpt($pid);
            if (!$excerpt) {
                $excerpt = wp_trim_words(wp_strip_all_tags(get_post_field('post_content', $pid)), 30, '…');
            }
            $img = get_the_post_thumbnail_url($pid, 'thumbnail') ?: '';
            $items[] = array(
                'id'          => $post_type . '-' . $pid,
                'label'       => get_the_title($pid),
                'url'         => get_permalink($pid),
                'level'       => 0,
                'excerpt'     => $excerpt,
                'image'       => $img,
            );
        }
        return $items;
    }

    private function build_blog_categories_group() {
        
        
        
        
        $parent_slug = (string) AIEO_DMM_Settings::get('navigator_blog_parent_slug', 'blog');
        $parent_id   = 0;
        if ($parent_slug !== '') {
            $term = get_term_by('slug', $parent_slug, 'category');
            if ($term && !is_wp_error($term)) {
                $parent_id = (int) $term->term_id;
            }
        }
        return array(
            'slug'  => 'blog-categories',
            'label' => __('Κατηγορίες Blog', 'ai-eshop-optimizer'),
            'icon'  => 'folder',
            'items' => $this->terms_to_items('category', 2, $parent_id),
        );
    }

    private function build_actions_group() {
        $items = array();
        foreach ($this->default_actions() as $a) {
            $items[] = array_merge(array('level' => 0), $a);
        }
        return array(
            'slug'  => 'actions',
            'label' => __('Εργαλεία', 'ai-eshop-optimizer'),
            'icon'  => 'zap',
            'items' => $items,
        );
    }

    
    private function terms_to_items($taxonomy, $max_depth = 3, $root_parent_id = 0) {
        if (!taxonomy_exists($taxonomy)) {
            return array();
        }
        
        
        
        $this->build_term_url_map($taxonomy);
        
        
        
        
        $terms = get_terms(array(
            'taxonomy'   => $taxonomy,
            'hide_empty' => true,
            'orderby'    => 'name',
            'order'      => 'ASC',
        ));
        
        
        
        
        
        
        
        if (is_wp_error($terms) || empty($terms)) {
            $terms = get_terms(array(
                'taxonomy'   => $taxonomy,
                'hide_empty' => false,
                'orderby'    => 'name',
                'order'      => 'ASC',
            ));
        }
        
        
        
        
        
        
        
        if (is_wp_error($terms) || empty($terms)) {
            global $wpdb;
            
            $rows = $wpdb->get_results($wpdb->prepare(
                "SELECT t.term_id, t.name, t.slug, tt.parent, tt.count, tt.description, tt.taxonomy
                 FROM {$wpdb->terms} t
                 INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_id = t.term_id
                 WHERE tt.taxonomy = %s
                 ORDER BY t.name ASC",
                $taxonomy
            ));
            $terms = array();
            if (is_array($rows)) {
                foreach ($rows as $r) {
                    $t = new stdClass();
                    $t->term_id     = (int) $r->term_id;
                    $t->name        = (string) $r->name;
                    $t->slug        = (string) $r->slug;
                    $t->parent      = (int) $r->parent;
                    $t->count       = (int) $r->count;
                    $t->description = (string) $r->description;
                    $t->taxonomy    = (string) $r->taxonomy;
                    $terms[] = $t;
                }
            }
        }
        if (is_wp_error($terms) || empty($terms)) {
            return array();
        }
        $excluded = $this->excluded_term_keys();
        
        $by_parent = array();
        foreach ($terms as $t) {
            $by_parent[(int) $t->parent][] = $t;
        }
        
        
        
        
        
        
        if ($taxonomy === 'product_cat' || $taxonomy === 'product_brand' || $taxonomy === 'pwb-brand') {
            $sales_by_id = $this->popularity_sales_map($taxonomy);
            if (!empty($sales_by_id)) {
                foreach ($by_parent as $pid => &$kids) {
                    usort($kids, function ($a, $b) use ($sales_by_id) {
                        $sa = isset($sales_by_id[(int) $a->term_id]) ? (float) $sales_by_id[(int) $a->term_id] : 0.0;
                        $sb = isset($sales_by_id[(int) $b->term_id]) ? (float) $sales_by_id[(int) $b->term_id] : 0.0;
                        if ($sa === $sb) {
                            
                            
                            
                            $ca = (int) $a->count;
                            $cb = (int) $b->count;
                            if ($ca !== $cb) return $cb - $ca;
                            return strcmp((string) $a->name, (string) $b->name);
                        }
                        return ($sa < $sb) ? 1 : -1;
                    });
                }
                unset($kids);
            }
        }
        
        
        
        if ($root_parent_id > 0) {
            $excluded[$taxonomy . ':1'] = true;
        }
        
        
        
        
        
        $thumb_map = $this->batch_term_thumbnails($terms);
        $out = array();
        $this->walk_terms($by_parent, (int) $root_parent_id, 0, $max_depth, $taxonomy, $out, null, $excluded, $thumb_map);
        return $out;
    }

    
    private function batch_term_thumbnails(array $terms) {
        if (empty($terms)) { return array(); }
        global $wpdb;
        $term_ids = array();
        foreach ($terms as $t) { $term_ids[] = (int) $t->term_id; }
        if (empty($term_ids)) { return array(); }
        $placeholders = implode(',', array_fill(0, count($term_ids), '%d'));
        
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT term_id, meta_value AS thumb_id
             FROM {$wpdb->termmeta}
             WHERE meta_key = 'thumbnail_id' AND term_id IN ({$placeholders}) AND meta_value > 0",
            ...$term_ids
        ));
        if (empty($rows)) { return array(); }
        $term_to_thumb = array();
        $thumb_ids     = array();
        foreach ($rows as $r) {
            $tid = (int) $r->thumb_id;
            if ($tid <= 0) { continue; }
            $term_to_thumb[(int) $r->term_id] = $tid;
            $thumb_ids[$tid] = true;
        }
        if (empty($thumb_ids)) { return array(); }
        $thumb_id_list = array_keys($thumb_ids);
        $ph2 = implode(',', array_fill(0, count($thumb_id_list), '%d'));
        
        $files = $wpdb->get_results($wpdb->prepare(
            "SELECT post_id, meta_value AS file
             FROM {$wpdb->postmeta}
             WHERE meta_key = '_wp_attached_file' AND post_id IN ({$ph2})",
            ...$thumb_id_list
        ));
        $upload_url = '';
        if (function_exists('wp_get_upload_dir')) {
            $u = wp_get_upload_dir();
            $upload_url = isset($u['baseurl']) ? trailingslashit((string) $u['baseurl']) : '';
        }
        $thumb_id_to_url = array();
        foreach ((array) $files as $r) {
            $thumb_id_to_url[(int) $r->post_id] = $upload_url . (string) $r->file;
        }
        $out = array();
        foreach ($term_to_thumb as $term_id => $thumb_id) {
            if (isset($thumb_id_to_url[$thumb_id])) {
                $out[$term_id] = $thumb_id_to_url[$thumb_id];
            }
        }
        return $out;
    }

    
    private function popularity_sales_map($taxonomy) {
        if (!$this->popularity_table_exists()) {
            return array();
        }
        global $wpdb;
        $tbl = $this->popularity_table();
        
        
        
        
        
        $order_by = (string) AIEO_DMM_Settings::get('navigator_order_metric', 'turnover');
        $col = ($order_by === 'units') ? 'total_sales' : 'total_turnover';
        $rows = array();
        if ($taxonomy === 'product_cat') {
            
            $rows = $wpdb->get_results(
                "SELECT category_id AS id, {$col} AS metric
                 FROM {$tbl}
                 WHERE category_id IS NOT NULL AND (brand_id IS NULL OR brand_id = 0)"
            );
        } elseif ($taxonomy === 'product_brand' || $taxonomy === 'pwb-brand') {
            
            $rows = $wpdb->get_results(
                "SELECT brand_id AS id, {$col} AS metric
                 FROM {$tbl}
                 WHERE (category_id IS NULL OR category_id = 0) AND brand_id IS NOT NULL AND brand_id > 0"
            );
        }
        $map = array();
        foreach ((array) $rows as $r) {
            $map[(int) $r->id] = (float) $r->metric;
        }
        return $map;
    }

    private function walk_terms(array $by_parent, $parent_id, $depth, $max_depth, $taxonomy, array &$out, $parent_key, array $excluded, array $thumb_map = array()) {
        if ($depth >= $max_depth || empty($by_parent[$parent_id])) {
            return;
        }
        foreach ($by_parent[$parent_id] as $t) {
            $key = $taxonomy . '-' . $t->term_id;
            
            
            if (isset($excluded[$taxonomy . ':' . (int) $t->term_id])) {
                continue;
            }
            
            
            
            
            
            
            
            
            
            if ((int) $t->count === 0) {
                continue;
            }
            
            
            
            $img = isset($thumb_map[(int) $t->term_id]) ? (string) $thumb_map[(int) $t->term_id] : '';
            $out[] = array(
                'id'          => $key,
                'label'       => $t->name,
                'url'         => $this->resolve_term_url($t, $taxonomy),
                'level'       => $depth,
                'parent'      => $parent_key,
                'count'       => (int) $t->count,
                'description' => wp_strip_all_tags((string) $t->description),
                'image'       => $img,
            );
            $this->walk_terms($by_parent, (int) $t->term_id, $depth + 1, $max_depth, $taxonomy, $out, $key, $excluded, $thumb_map);
        }
    }

    
    
    

    public function enqueue_assets() {
        $upload = wp_upload_dir();
        $json_path = trailingslashit($upload['basedir']) . 'aieo/navigator.json';
        if (!file_exists($json_path)) {
            
            $this->generate_json();
        }
        $version = (string) get_option('aieo_dmm_navigator_version', '1.0.0');
        $url     = trailingslashit($upload['baseurl']) . 'aieo/navigator.json?v=' . rawurlencode($version);

        
        
        
        $css_path = AIEO_DIR . 'assets/css/aieo-navigator.css';
        $js_path  = AIEO_DIR . 'assets/js/aieo-navigator.js';
        $css_ver  = file_exists($css_path) ? (string) filemtime($css_path) : $version;
        $js_ver   = file_exists($js_path)  ? (string) filemtime($js_path)  : $version;

        wp_enqueue_style(
            'aieo-dmm-navigator',
            AIEO_URI . 'assets/css/aieo-navigator.css',
            array(),
            $css_ver
        );
        wp_enqueue_script(
            'aieo-dmm-navigator',
            AIEO_URI . 'assets/js/aieo-navigator.js',
            array(),
            $js_ver,
            true
        );
        wp_localize_script('aieo-dmm-navigator', 'aieoNavigator', array(
            'jsonUrl'        => $url,
            'cacheVersion'   => $version,
            'context'        => $this->current_context(),
            
            
            
            
            'groupsOrder'    => $this->resolve_groups_config(),
            'i18n'           => array(
                'placeholder'   => __('Πληκτρολόγησε για αναζήτηση…', 'ai-eshop-optimizer'),
                'noResults'     => __('Δεν βρέθηκαν αποτελέσματα.', 'ai-eshop-optimizer'),
                'kbdNavigate'   => __('πλοήγηση', 'ai-eshop-optimizer'),
                'kbdOpen'       => __('άνοιγμα', 'ai-eshop-optimizer'),
                'kbdBack'       => __('πίσω', 'ai-eshop-optimizer'),
                'kbdClose'      => __('κλείσιμο', 'ai-eshop-optimizer'),
                'children'      => __('Υποκατηγορίες', 'ai-eshop-optimizer'),
                'open'          => __('Άνοιγμα', 'ai-eshop-optimizer'),
                'topBrands'     => __('Δημοφιλή brands', 'ai-eshop-optimizer'),
                'topNeeds'      => __('Δημοφιλείς ανάγκες', 'ai-eshop-optimizer'),
                'categoryNeeds' => __('Ανάγκες αυτής της κατηγορίας', 'ai-eshop-optimizer'),
                'categoryBrands' => __('Μάρκες αυτής της κατηγορίας', 'ai-eshop-optimizer'),
                'filters'       => __('Φίλτρα', 'ai-eshop-optimizer'),
            ),
        ));
    }

    
    private function current_context() {
        $boost = array();
        if (function_exists('is_product') && is_product()) {
            $pid = get_queried_object_id();
            if ($pid) {
                
                $cats = wp_get_object_terms($pid, 'product_cat', array('fields' => 'ids'));
                if (!is_wp_error($cats)) {
                    foreach ($cats as $cid) { $boost[] = 'product_cat-' . (int) $cid; }
                }
                
                $brands = wp_get_object_terms($pid, 'product_brand', array('fields' => 'ids'));
                if (!is_wp_error($brands) && !empty($brands)) {
                    foreach ($brands as $bid) { $boost[] = 'brand-' . (int) $bid; }
                }
            }
        } elseif (function_exists('is_product_category') && is_product_category()) {
            $term = get_queried_object();
            if ($term && isset($term->term_id)) {
                $boost[] = 'product_cat-' . (int) $term->term_id;
                
                $parent = (int) $term->parent;
                while ($parent > 0) {
                    $boost[] = 'product_cat-' . $parent;
                    $p = get_term($parent, 'product_cat');
                    $parent = $p && !is_wp_error($p) ? (int) $p->parent : 0;
                }
            }
        } elseif (is_singular('post')) {
            $cats = wp_get_object_terms(get_queried_object_id(), 'category', array('fields' => 'ids'));
            if (!is_wp_error($cats)) {
                foreach ($cats as $cid) { $boost[] = 'category-' . (int) $cid; }
            }
        } elseif (is_category()) {
            $term = get_queried_object();
            if ($term && isset($term->term_id)) {
                $boost[] = 'category-' . (int) $term->term_id;
            }
        } elseif (function_exists('is_tax')
            && is_tax(array('pa_need', 'pa-need', 'product_brand', 'pwb-brand'))
            && ! empty($_GET['in_cat'])) { 
            
            
            $cat_id = absint(wp_unslash($_GET['in_cat'])); 
            if ($cat_id > 0) {
                $boost[] = 'product_cat-' . $cat_id;
                $p      = get_term($cat_id, 'product_cat');
                $parent = ($p && !is_wp_error($p)) ? (int) $p->parent : 0;
                $guard  = 0;
                while ($parent > 0 && $guard < 32) {
                    $boost[] = 'product_cat-' . $parent;
                    $pp      = get_term($parent, 'product_cat');
                    $parent  = ($pp && !is_wp_error($pp)) ? (int) $pp->parent : 0;
                    $guard++;
                }
            }
        }
        $ctx = array('boost' => array_values(array_unique($boost)));
        
        
        
        
        
        if ($this->needs_scoped_enabled()) {
            $ctx['needsScoped'] = true;
            $scope_cat = $this->resolve_scope_category();
            if ($scope_cat) {
                $ctx['category'] = $scope_cat;
            }
        }
        return $ctx;
    }

    
    private function resolve_scope_category() {
        if (function_exists('is_product_category') && is_product_category()) {
            $term = get_queried_object();
            if ($term && isset($term->term_id, $term->slug)) {
                return array('id' => (int) $term->term_id, 'slug' => (string) $term->slug);
            }
        } elseif (function_exists('is_product') && is_product()) {
            $cat_id = $this->primary_product_cat_id(get_queried_object_id());
            if ($cat_id > 0) {
                $term = get_term($cat_id, 'product_cat');
                if ($term && !is_wp_error($term)) {
                    return array('id' => (int) $term->term_id, 'slug' => (string) $term->slug);
                }
            }
        } elseif (function_exists('is_tax')
            && is_tax(array('pa_need', 'pa-need', 'product_brand', 'pwb-brand'))
            && ! empty($_GET['in_cat'])) { 
            
            
            $cat_id = absint(wp_unslash($_GET['in_cat'])); 
            if ($cat_id > 0) {
                $term = get_term($cat_id, 'product_cat');
                if ($term && !is_wp_error($term)) {
                    return array('id' => (int) $term->term_id, 'slug' => (string) $term->slug);
                }
            }
        }
        return null;
    }

    
    private function primary_product_cat_id($product_id) {
        $product_id = (int) $product_id;
        if ($product_id <= 0) { return 0; }
        
        if (class_exists('AIEO_SEO_Store')) {
            $cat_id = (int) AIEO_SEO_Store::get_primary_cat_id('product', $product_id);
            if ($cat_id > 0) {
                $t = get_term($cat_id, 'product_cat');
                if ($t && !is_wp_error($t)) { return $cat_id; }
            }
        }
        
        $primary = (int) get_post_meta($product_id, '_yoast_wpseo_primary_product_cat', true);
        if ($primary > 0) {
            $t = get_term($primary, 'product_cat');
            if ($t && !is_wp_error($t)) { return $primary; }
        }
        
        $cats = wp_get_object_terms($product_id, 'product_cat', array('fields' => 'ids'));
        if (!is_wp_error($cats) && !empty($cats)) {
            return (int) $cats[0];
        }
        return 0;
    }

    
    private function shell_style_vars() {
        $bw  = (int) AIEO_DMM_Settings::get('navigator_border_width',  1);
        $br  = (int) AIEO_DMM_Settings::get('navigator_border_radius', 12);
        $bc  = (string) AIEO_DMM_Settings::get('navigator_border_color', '#e5e7eb');
        $bg  = (string) AIEO_DMM_Settings::get('navigator_bg_color',     '');
        $fg  = (string) AIEO_DMM_Settings::get('navigator_fg_color',     '');
        $acc = (string) AIEO_DMM_Settings::get('navigator_accent_color', '');
        $pad = (int)    AIEO_DMM_Settings::get('navigator_padding',      16);
        $pairs = array(
            '--aieo-nav-border-width:'  . $bw . 'px',
            '--aieo-nav-border-radius:' . $br . 'px',
            '--aieo-nav-border-color:'  . $bc,
            '--aieo-nav-padding:'       . $pad . 'px',
        );
        
        
        if ($bg  !== '') { $pairs[] = '--aieo-nav-bg:'     . $bg; }
        if ($fg  !== '') { $pairs[] = '--aieo-nav-fg:'     . $fg; }
        if ($acc !== '') { $pairs[] = '--aieo-nav-accent:' . $acc; }
        $sel_bg = (string) AIEO_DMM_Settings::get('navigator_selection_bg', '');
        $hov_bg = (string) AIEO_DMM_Settings::get('navigator_hover_bg',     '');
        if ($sel_bg !== '') { $pairs[] = '--aieo-nav-selection-bg:' . $sel_bg; }
        if ($hov_bg !== '') { $pairs[] = '--aieo-nav-hover-bg:'     . $hov_bg; }
        $count_bg = (string) AIEO_DMM_Settings::get('navigator_count_bg', '');
        $count_fg = (string) AIEO_DMM_Settings::get('navigator_count_fg', '');
        if ($count_bg !== '') { $pairs[] = '--aieo-nav-count-bg:' . $count_bg; }
        if ($count_fg !== '') { $pairs[] = '--aieo-nav-count-fg:' . $count_fg; }
        return implode(';', $pairs);
    }

    
    private function mobile_visibility_css() {
        $hide_right = (bool) AIEO_DMM_Settings::get('navigator_mobile_hide_right_pane', false);
        $hide_needs = (bool) AIEO_DMM_Settings::get('navigator_mobile_hide_top_needs',  false);
        $hide_brand = (bool) AIEO_DMM_Settings::get('navigator_mobile_hide_top_brands', false);
        $bp_px = (int) AIEO_DMM_Settings::get('navigator_mobile_breakpoint', 768);
        if ($bp_px < 320 || $bp_px > 1280) { $bp_px = 768; }

        $rules = array();
        if ($hide_right) {
            
            $rules[] = '.aieo-navigator-body .aieo-navigator-right{display:none !important;}';
            $rules[] = '.aieo-navigator-body{grid-template-columns:1fr !important;}';
        }
        if ($hide_needs) {
            
            
            
            $rules[] = '.aieo-nav-highlight-section[data-section="needs"]{display:none !important;}';
        }
        if ($hide_brand) {
            $rules[] = '.aieo-nav-highlight-section[data-section="brands"]{display:none !important;}';
        }
        if (empty($rules)) { return ''; }
        return '@media (max-width:' . $bp_px . 'px){' . implode('', $rules) . '}';
    }

    public function render_modal_shell() {
        if (wp_is_mobile()) {
            
            
        }
        $style = $this->shell_style_vars();
        
        
        
        $count_pill = (AIEO_DMM_Settings::get('navigator_count_style', 'plain') === 'pill')
            || ('' !== (string) AIEO_DMM_Settings::get('navigator_count_bg', ''))
            || ('' !== (string) AIEO_DMM_Settings::get('navigator_count_fg', ''));
        
        
        $mobile_css = $this->mobile_visibility_css();
        if ($mobile_css !== '') {
            echo '<style id="aieo-navigator-mobile-rules">' . $mobile_css . '</style>'; 
        }
        ?>
        <div id="aieo-navigator-root" class="aieo-navigator-root<?php echo $count_pill ? ' is-count-pill' : ''; ?>" role="dialog" aria-modal="true" aria-labelledby="aieo-navigator-title" style="<?php echo esc_attr($style); ?>" hidden>
            <div class="aieo-navigator-backdrop" data-aieo-navigator-close></div>
            <div class="aieo-navigator-panel" role="document">
                <header class="aieo-navigator-header">
                    <input
                        type="search"
                        class="aieo-navigator-input"
                        placeholder="<?php esc_attr_e('Βρες κατηγορία, brand, υπηρεσία ή σελίδα', 'ai-eshop-optimizer'); ?>"
                        aria-label="<?php esc_attr_e('Πλοήγηση καταλόγου', 'ai-eshop-optimizer'); ?>"
                        autocomplete="off"
                        spellcheck="false"
                    />
                    <span class="aieo-navigator-breadcrumb" aria-live="polite"></span>
                    <button type="button" class="aieo-navigator-close" data-aieo-navigator-close aria-label="<?php esc_attr_e('Κλείσιμο', 'ai-eshop-optimizer'); ?>">
                        <!-- Drawn from line endpoints — mathematically centered. The
                             unicode `×` glyph sits below the optical centre in most
                             fonts (its bbox is on the math baseline, not the visual
                             middle); SVG strokes have no such drift. -->
                        <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false">
                            <line x1="2" y1="2" x2="12" y2="12"></line>
                            <line x1="12" y1="2" x2="2" y2="12"></line>
                        </svg>
                    </button>
                </header>
                <div class="aieo-navigator-body" role="listbox"></div>
                <footer class="aieo-navigator-footer">
                    <span><kbd>↑</kbd><kbd>↓</kbd> <?php esc_html_e('πλοήγηση', 'ai-eshop-optimizer'); ?></span>
                    <span><kbd>↵</kbd> <?php esc_html_e('άνοιγμα', 'ai-eshop-optimizer'); ?></span>
                    <span><kbd>⌫</kbd> <?php esc_html_e('πίσω', 'ai-eshop-optimizer'); ?></span>
                    <span><kbd>Esc</kbd> <?php esc_html_e('κλείσιμο', 'ai-eshop-optimizer'); ?></span>
                </footer>
            </div>
        </div>
        <?php
    }

    
    
    

    public function ajax_save_settings() {
        AIEO_DMM::verify_ajax_request();
        $enabled = !empty($_POST['enabled']) ? true : false;
        
        
        $modules = (array) AIEO_DMM_Settings::get('enabled_modules', array());
        $modules['navigator'] = $enabled;
        $patch = array('enabled_modules' => $modules);
        if (isset($_POST['excluded_terms'])) {
            
            
            
            
            $patch['navigator_excluded_terms'] = sanitize_textarea_field( wp_unslash( (string) $_POST['excluded_terms'] ) );
        }
        if (isset($_POST['daily_enabled'])) {
            $patch['navigator_daily_enabled'] = !empty($_POST['daily_enabled']);
        }
        if (isset($_POST['daily_time'])) {
            $t = sanitize_text_field(wp_unslash($_POST['daily_time']));
            if (preg_match('/^([01]\d|2[0-3]):[0-5]\d$/', $t)) {
                $patch['navigator_daily_time'] = $t;
            }
        }
        
        if (isset($_POST['blog_parent_slug'])) {
            $patch['navigator_blog_parent_slug'] = sanitize_title(wp_unslash($_POST['blog_parent_slug']));
        }
        
        if (isset($_POST['order_metric'])) {
            $om = sanitize_key(wp_unslash($_POST['order_metric']));
            $patch['navigator_order_metric'] = in_array($om, array('turnover', 'units'), true) ? $om : 'turnover';
        }
        
        
        
        
        if (isset($_POST['needs_scoped'])) {
            $patch['navigator_needs_scoped'] = !empty($_POST['needs_scoped']) ? 1 : 0;
        }
        
        
        if (isset($_POST['groups_config'])) {
            $raw = wp_unslash((string) sanitize_text_field( wp_unslash( $_POST['groups_config'] ) ));
            $rows = json_decode($raw, true);
            if (is_array($rows)) {
                $known = array();
                foreach (self::group_catalog() as $g) { $known[$g['slug']] = true; }
                $clean = array();
                $seen  = array();
                foreach ($rows as $r) {
                    if (!is_array($r) || empty($r['slug'])) { continue; }
                    $slug  = sanitize_key($r['slug']);
                    $place = isset($r['placement']) ? sanitize_key($r['placement']) : 'main';
                    if (!isset($known[$slug]) || isset($seen[$slug])) { continue; }
                    if (!in_array($place, array('main', 'rail', 'off'), true)) { $place = 'main'; }
                    $clean[] = array('slug' => $slug, 'placement' => $place);
                    $seen[$slug] = true;
                }
                $patch['navigator_groups_config'] = $clean;
            }
        }
        
        
        
        $color_keys = array(
            'border_color' => 'navigator_border_color',
            'bg_color'     => 'navigator_bg_color',
            'fg_color'     => 'navigator_fg_color',
            'accent_color' => 'navigator_accent_color',
            'selection_bg' => 'navigator_selection_bg',
            'hover_bg'     => 'navigator_hover_bg',
            'count_bg'     => 'navigator_count_bg',
            'count_fg'     => 'navigator_count_fg',
        );
        foreach ($color_keys as $post_key => $opt_key) {
            if (!isset($_POST[$post_key])) { continue; }
            $raw_val = trim((string) wp_unslash($_POST[$post_key]));
            
            if ($raw_val === '') {
                $patch[$opt_key] = '';
                continue;
            }
            
            $val = class_exists('AIEO_Admin_Color') ? AIEO_Admin_Color::sanitize($raw_val) : (string) sanitize_hex_color($raw_val);
            $patch[$opt_key] = ($val !== '') ? $val : (string) AIEO_DMM_Settings::get($opt_key, '');
        }
        if (isset($_POST['count_style'])) {
            $cs = sanitize_key(wp_unslash((string) $_POST['count_style']));
            $patch['navigator_count_style'] = ($cs === 'pill') ? 'pill' : 'plain';
        }
        if (isset($_POST['border_width']))  { $patch['navigator_border_width']  = max(0, min(20, (int) $_POST['border_width'])); }
        if (isset($_POST['border_radius'])) { $patch['navigator_border_radius'] = max(0, min(64, (int) $_POST['border_radius'])); }
        if (isset($_POST['padding']))       { $patch['navigator_padding']       = max(0, min(64, (int) $_POST['padding'])); }
        
        if (isset($_POST['mobile_hide_right_pane']) || isset($_POST['mobile_visibility_form'])) {
            $patch['navigator_mobile_hide_right_pane'] = !empty($_POST['mobile_hide_right_pane']) ? 1 : 0;
        }
        if (isset($_POST['mobile_hide_top_needs']) || isset($_POST['mobile_visibility_form'])) {
            $patch['navigator_mobile_hide_top_needs']  = !empty($_POST['mobile_hide_top_needs']) ? 1 : 0;
        }
        if (isset($_POST['mobile_hide_top_brands']) || isset($_POST['mobile_visibility_form'])) {
            $patch['navigator_mobile_hide_top_brands'] = !empty($_POST['mobile_hide_top_brands']) ? 1 : 0;
        }
        if (isset($_POST['mobile_breakpoint'])) {
            $patch['navigator_mobile_breakpoint'] = max(320, min(1280, (int) $_POST['mobile_breakpoint']));
        }
        AIEO_DMM_Settings::update($patch);
        
        $this->ensure_cron_scheduled();
        
        
        
        $this->generate_json();
        wp_send_json_success(array(
            'enabled'       => $enabled ? 1 : 0,
            'next_run'      => wp_next_scheduled(self::CRON_HOOK),
        ));
    }

    public function ajax_regenerate() {
        AIEO_DMM::verify_ajax_request();
        
        
        if (isset($_POST['excluded_terms'])) {
            AIEO_DMM_Settings::update(array(
                'navigator_excluded_terms' => sanitize_textarea_field( wp_unslash( (string) $_POST['excluded_terms'] ) ),
            ));
        }
        
        
        
        
        
        
        
        
        
        
        
        global $wp_filter;
        $saved_clauses = isset($wp_filter['terms_clauses']) ? $wp_filter['terms_clauses'] : null;
        $saved_pre     = isset($wp_filter['pre_get_terms'])  ? $wp_filter['pre_get_terms']  : null;
        if ($saved_clauses !== null) { unset($wp_filter['terms_clauses']); }
        if ($saved_pre     !== null) { unset($wp_filter['pre_get_terms']); }

        
        
        
        $ok = $this->generate_json(true);

        
        if ($saved_clauses !== null) { $wp_filter['terms_clauses'] = $saved_clauses; }
        if ($saved_pre     !== null) { $wp_filter['pre_get_terms']  = $saved_pre; }

        if (!$ok) {
            
            
            
            $abort = get_transient('aieo_dmm_navigator_last_abort');
            if ($abort && !empty($abort['missing'])) {
                wp_send_json_error(array(
                    'message' => sprintf(
                        
                        __('Regeneration aborted (existing JSON preserved). These spine groups came back empty: %s. The on-disk file is unchanged. Try `wp eval \'AIEO_DMM::instance()->get_module("navigator")->generate_json();\'` from CLI to bypass admin-side query filters.', 'ai-eshop-optimizer'),
                        implode(', ', $abort['missing'])
                    ),
                ));
                return;
            }
            wp_send_json_error(array('message' => __('Regeneration failed. Check error log.', 'ai-eshop-optimizer')));
        }
        
        delete_transient('aieo_dmm_navigator_last_abort');
        wp_send_json_success(array(
            'version'      => get_option('aieo_dmm_navigator_version', '1.0.0'),
            'generated_at' => get_option('aieo_dmm_navigator_generated_at', 0),
        ));
    }

    
    
    

    public function enqueue_admin_assets() {
        
        
        if (!class_exists('AIEO_Admin_Color') && defined('AIEO_DIR') && file_exists(AIEO_DIR . 'includes/class-aieo-admin-color.php')) {
            require_once AIEO_DIR . 'includes/class-aieo-admin-color.php';
        }
        if (class_exists('AIEO_Admin_Color')) {
            AIEO_Admin_Color::enqueue();
        } else {
            wp_enqueue_style('wp-color-picker');
            wp_enqueue_script('wp-color-picker');
            wp_add_inline_script('wp-color-picker', 'jQuery(function($){$(".aieo-nav-color").wpColorPicker();});');
        }
        wp_enqueue_script('jquery-ui-sortable');
    }

    public function render_subtab() {
        $enabled = $this->is_enabled();
        $excluded = (string) AIEO_DMM_Settings::get('navigator_excluded_terms', '');
        $version = (string) get_option('aieo_dmm_navigator_version', '1.0.0');
        $generated_at = (int) get_option('aieo_dmm_navigator_generated_at', 0);
        $upload = wp_upload_dir();
        $json_path = trailingslashit($upload['basedir']) . 'aieo/navigator.json';
        $exists = file_exists($json_path);
        $size = $exists ? size_format(filesize($json_path)) : '—';
        $daily_enabled = $this->daily_enabled();
        $daily_time    = $this->daily_time();
        $next_run_ts   = wp_next_scheduled(self::CRON_HOOK);
        $next_run_label = $next_run_ts
            ? wp_date(get_option('date_format') . ' ' . get_option('time_format'), $next_run_ts)
            : __('—', 'ai-eshop-optimizer');

        
        $catalog        = self::group_catalog();
        $catalog_labels = array();
        foreach ($catalog as $g) { $catalog_labels[$g['slug']] = $g['label']; }
        $groups_config = $this->resolve_groups_config();

        
        
        $blog_parent_slug = (string) AIEO_DMM_Settings::get('navigator_blog_parent_slug', 'blog');
        $order_metric     = (string) AIEO_DMM_Settings::get('navigator_order_metric', 'turnover');
        $needs_scoped     = (bool)   AIEO_DMM_Settings::get('navigator_needs_scoped', false);
        $bw  = (int)    AIEO_DMM_Settings::get('navigator_border_width',  1);
        $br  = (int)    AIEO_DMM_Settings::get('navigator_border_radius', 12);
        $pad = (int)    AIEO_DMM_Settings::get('navigator_padding',       16);
        $bc  = (string) AIEO_DMM_Settings::get('navigator_border_color',  '#e5e7eb');
        $bg  = (string) AIEO_DMM_Settings::get('navigator_bg_color',     '');
        $fg  = (string) AIEO_DMM_Settings::get('navigator_fg_color',     '');
        $acc = (string) AIEO_DMM_Settings::get('navigator_accent_color', '');
        $selbg = (string) AIEO_DMM_Settings::get('navigator_selection_bg', '');
        $hovbg = (string) AIEO_DMM_Settings::get('navigator_hover_bg',     '');
        $count_style = (string) AIEO_DMM_Settings::get('navigator_count_style', 'plain');
        $count_bg    = (string) AIEO_DMM_Settings::get('navigator_count_bg',    '');
        $count_fg    = (string) AIEO_DMM_Settings::get('navigator_count_fg',    '');
        ?>
        <div class="aieo-dmm-subtab aieo-dmm-navigator">
            <h2><?php esc_html_e('Navigator (Καταλογος)', 'ai-eshop-optimizer'); ?></h2>
            <p class="description">
                <?php esc_html_e('Ctrl+K / ⌘K command palette that surfaces all categories, brands, needs, pages, posts, and functional shortcuts. JSON is auto-regenerated whenever a watched content type changes.', 'ai-eshop-optimizer'); ?>
            </p>

            <table class="form-table" role="presentation">
                <tbody>
                    <tr>
                        <th scope="row"><?php esc_html_e('Status', 'ai-eshop-optimizer'); ?></th>
                        <td>
                            <label>
                                <input type="checkbox" id="aieo-navigator-enabled" <?php checked($enabled); ?> />
                                <?php esc_html_e('Enabled', 'ai-eshop-optimizer'); ?>
                            </label>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><?php esc_html_e('JSON file', 'ai-eshop-optimizer'); ?></th>
                        <td>
                            <?php if ($exists): ?>
                                <code>wp-content/uploads/aieo/navigator.json</code>
                                <span style="color:#46b450;"> ✓ <?php echo esc_html($size); ?></span>
                                <br/>
                                <span class="description">
                                    <?php
                                    printf(
                                        
                                        esc_html__('Version %1$s — generated %2$s ago', 'ai-eshop-optimizer'),
                                        esc_html($version),
                                        esc_html(human_time_diff($generated_at))
                                    );
                                    ?>
                                </span>
                            <?php else: ?>
                                <span style="color:#dc3232;">✗ <?php esc_html_e('Not yet generated', 'ai-eshop-optimizer'); ?></span>
                            <?php endif; ?>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row">
                            <label for="aieo-navigator-excluded"><?php esc_html_e('Hidden terms', 'ai-eshop-optimizer'); ?></label>
                        </th>
                        <td>
                            <textarea
                                id="aieo-navigator-excluded"
                                rows="3"
                                cols="50"
                                placeholder="42, 1234, product_brand:99"
                                style="font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;"
                            ><?php echo esc_textarea($excluded); ?></textarea>
                            <p class="description">
                                <?php esc_html_e('Comma- or whitespace-separated term IDs to omit from the navigator (and all their descendants). Bare integers default to product_cat. For other taxonomies use `taxonomy:id`, e.g. `product_brand:99`.', 'ai-eshop-optimizer'); ?>
                            </p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><?php esc_html_e('Group order & placement', 'ai-eshop-optimizer'); ?></th>
                        <td>
                            <p class="description" style="margin:0 0 8px;">
                                <?php esc_html_e('Drag to reorder. "Main" = primary list column. "Right rail" = compact side strip (good for Tools / shortcuts). "Off" hides the group entirely.', 'ai-eshop-optimizer'); ?>
                            </p>
                            <style>
                                .aieo-nav-groups-placeholder {
                                    visibility: visible !important;
                                    background: #fef3c7;
                                    border: 1px dashed #f59e0b;
                                    border-radius: 6px;
                                    margin: 0 0 6px;
                                    height: 42px;
                                }
                                .aieo-nav-groups-item.ui-sortable-helper {
                                    background: #fff !important;
                                    box-shadow: 0 8px 22px rgba(0,0,0,0.12);
                                }
                            </style>
                            <ul id="aieo-nav-groups-list" class="aieo-nav-groups-list" style="margin:0;padding:0;list-style:none;max-width:560px;">
                                <?php foreach ($groups_config as $cfg) :
                                    $label = isset($catalog_labels[$cfg['slug']]) ? $catalog_labels[$cfg['slug']] : $cfg['slug'];
                                ?>
                                <li class="aieo-nav-groups-item" data-slug="<?php echo esc_attr($cfg['slug']); ?>"
                                    style="display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid #e5e7eb;border-radius:6px;margin:0 0 6px;background:#fff;">
                                    <span class="aieo-nav-drag" style="cursor:grab;color:#9ca3af;font-weight:700;letter-spacing:1px;">⋮⋮</span>
                                    <span class="aieo-nav-group-label" style="flex:1 1 auto;font-weight:500;"><?php echo esc_html($label); ?></span>
                                    <select class="aieo-nav-placement" style="min-width:140px;">
                                        <option value="main" <?php selected($cfg['placement'], 'main'); ?>><?php esc_html_e('Main', 'ai-eshop-optimizer'); ?></option>
                                        <option value="rail" <?php selected($cfg['placement'], 'rail'); ?>><?php esc_html_e('Right rail', 'ai-eshop-optimizer'); ?></option>
                                        <option value="off"  <?php selected($cfg['placement'], 'off');  ?>><?php esc_html_e('Off', 'ai-eshop-optimizer'); ?></option>
                                    </select>
                                </li>
                                <?php endforeach; ?>
                            </ul>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row">
                            <label for="aieo-nav-blog-parent"><?php esc_html_e('Blog parent slug', 'ai-eshop-optimizer'); ?></label>
                        </th>
                        <td>
                            <input type="text" id="aieo-nav-blog-parent" value="<?php echo esc_attr($blog_parent_slug); ?>" class="regular-text" />
                            <p class="description">
                                <?php esc_html_e('The Blog Categories group lists only this term\'s direct children, skipping the parent itself and "Uncategorized". Default: blog. Leave empty to show every category at the WP root.', 'ai-eshop-optimizer'); ?>
                            </p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row">
                            <label for="aieo-nav-order-metric"><?php esc_html_e('Order metric', 'ai-eshop-optimizer'); ?></label>
                        </th>
                        <td>
                            <select id="aieo-nav-order-metric">
                                <option value="turnover" <?php selected($order_metric, 'turnover'); ?>><?php esc_html_e('Turnover (revenue) — default', 'ai-eshop-optimizer'); ?></option>
                                <option value="units"    <?php selected($order_metric, 'units'); ?>><?php esc_html_e('Unit sales (quantity sold)', 'ai-eshop-optimizer'); ?></option>
                            </select>
                            <p class="description">
                                <?php esc_html_e('Which metric ranks subcategories + brands in the navigator. "Turnover" puts the highest-revenue items first (good for surfacing premium ranges); "Unit sales" puts the most-bought items first (good for surfacing fast-moving SKUs). Both columns are populated nightly by the popularity aggregator.', 'ai-eshop-optimizer'); ?>
                            </p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><?php esc_html_e('Needs scope', 'ai-eshop-optimizer'); ?></th>
                        <td>
                            <label>
                                <input type="checkbox" id="aieo-nav-needs-scoped" <?php checked($needs_scoped); ?> />
                                <?php esc_html_e('Scope needs to the current product category', 'ai-eshop-optimizer'); ?>
                            </label>
                            <p class="description">
                                <?php esc_html_e('When on: inside a product category (or on a product page) the Needs group shows only the needs that have products in that category and its subcategories, and clicking a need opens its archive limited to that category subtree. Off pages with no category (home, search) keep the full global needs list. When off, the navigator shows the global needs list everywhere — the original behaviour. Changing this rebuilds the navigator file.', 'ai-eshop-optimizer'); ?>
                            </p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><?php esc_html_e('Appearance', 'ai-eshop-optimizer'); ?></th>
                        <td>
                            <table class="widefat" style="border:0;background:transparent;max-width:560px;">
                                <tbody>
                                    <tr>
                                        <td style="padding:6px 8px 6px 0;border:0;width:140px;"><?php esc_html_e('Background', 'ai-eshop-optimizer'); ?></td>
                                        <td style="padding:6px 0;border:0;"><input type="text" name="bg_color"     value="<?php echo esc_attr($bg);  ?>" class="aieo-nav-color" data-default-color=""></td>
                                    </tr>
                                    <tr>
                                        <td style="padding:6px 8px 6px 0;border:0;"><?php esc_html_e('Text', 'ai-eshop-optimizer'); ?></td>
                                        <td style="padding:6px 0;border:0;"><input type="text" name="fg_color"     value="<?php echo esc_attr($fg);  ?>" class="aieo-nav-color" data-default-color=""></td>
                                    </tr>
                                    <tr>
                                        <td style="padding:6px 8px 6px 0;border:0;"><?php esc_html_e('Accent (links)', 'ai-eshop-optimizer'); ?></td>
                                        <td style="padding:6px 0;border:0;"><input type="text" name="accent_color" value="<?php echo esc_attr($acc); ?>" class="aieo-nav-color" data-default-color=""></td>
                                    </tr>
                                    <tr>
                                        <td style="padding:6px 8px 6px 0;border:0;"><?php esc_html_e('Border color', 'ai-eshop-optimizer'); ?></td>
                                        <td style="padding:6px 0;border:0;"><input type="text" name="border_color" value="<?php echo esc_attr($bc);  ?>" class="aieo-nav-color" data-default-color="#e5e7eb"></td>
                                    </tr>
                                    <tr>
                                        <td style="padding:6px 8px 6px 0;border:0;"><?php esc_html_e('Selection background', 'ai-eshop-optimizer'); ?></td>
                                        <td style="padding:6px 0;border:0;"><input type="text" name="selection_bg" value="<?php echo esc_attr($selbg); ?>" class="aieo-nav-color" data-default-color=""></td>
                                    </tr>
                                    <tr>
                                        <td style="padding:6px 8px 6px 0;border:0;"><?php esc_html_e('Hover background', 'ai-eshop-optimizer'); ?></td>
                                        <td style="padding:6px 0;border:0;"><input type="text" name="hover_bg" value="<?php echo esc_attr($hovbg); ?>" class="aieo-nav-color" data-default-color=""></td>
                                    </tr>
                                    <tr>
                                        <td style="padding:6px 8px 6px 0;border:0;"><?php esc_html_e('Category counts', 'ai-eshop-optimizer'); ?></td>
                                        <td style="padding:6px 0;border:0;">
                                            <select name="count_style">
                                                <option value="plain" <?php selected($count_style, 'plain'); ?>><?php esc_html_e('Plain numbers', 'ai-eshop-optimizer'); ?></option>
                                                <option value="pill"  <?php selected($count_style, 'pill');  ?>><?php esc_html_e('Pill (badge)', 'ai-eshop-optimizer'); ?></option>
                                            </select>
                                        </td>
                                    </tr>
                                    <tr>
                                        <td style="padding:6px 8px 6px 0;border:0;"><?php esc_html_e('Count pill background', 'ai-eshop-optimizer'); ?></td>
                                        <td style="padding:6px 0;border:0;"><input type="text" name="count_bg" value="<?php echo esc_attr($count_bg); ?>" class="aieo-nav-color" data-default-color=""></td>
                                    </tr>
                                    <tr>
                                        <td style="padding:6px 8px 6px 0;border:0;"><?php esc_html_e('Count pill text', 'ai-eshop-optimizer'); ?></td>
                                        <td style="padding:6px 0;border:0;"><input type="text" name="count_fg" value="<?php echo esc_attr($count_fg); ?>" class="aieo-nav-color" data-default-color=""></td>
                                    </tr>
                                    <tr>
                                        <td style="padding:6px 8px 6px 0;border:0;"><?php esc_html_e('Border width (px)', 'ai-eshop-optimizer'); ?></td>
                                        <td style="padding:6px 0;border:0;"><input type="number" min="0" max="20" name="border_width"  value="<?php echo (int) $bw;  ?>" class="small-text aieo-nav-num"></td>
                                    </tr>
                                    <tr>
                                        <td style="padding:6px 8px 6px 0;border:0;"><?php esc_html_e('Border radius (px)', 'ai-eshop-optimizer'); ?></td>
                                        <td style="padding:6px 0;border:0;"><input type="number" min="0" max="64" name="border_radius" value="<?php echo (int) $br;  ?>" class="small-text aieo-nav-num"></td>
                                    </tr>
                                    <tr>
                                        <td style="padding:6px 8px 6px 0;border:0;"><?php esc_html_e('Inner padding (px)', 'ai-eshop-optimizer'); ?></td>
                                        <td style="padding:6px 0;border:0;"><input type="number" min="0" max="64" name="padding"       value="<?php echo (int) $pad; ?>" class="small-text aieo-nav-num"></td>
                                    </tr>
                                </tbody>
                            </table>
                            <p class="description" style="margin-top:6px;">
                                <?php esc_html_e('Color fields left empty fall back to theme tokens (--wp--preset--color--bg / fg / accent / border). Hard-coded values override the theme.', 'ai-eshop-optimizer'); ?>
                            </p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><?php esc_html_e('Mobile visibility', 'ai-eshop-optimizer'); ?></th>
                        <td>
                            <?php
                            $mb_hide_right  = (bool) AIEO_DMM_Settings::get('navigator_mobile_hide_right_pane', false);
                            $mb_hide_needs  = (bool) AIEO_DMM_Settings::get('navigator_mobile_hide_top_needs',  false);
                            $mb_hide_brands = (bool) AIEO_DMM_Settings::get('navigator_mobile_hide_top_brands', false);
                            $mb_breakpoint  = (int)  AIEO_DMM_Settings::get('navigator_mobile_breakpoint', 768);
                            ?>
                            <input type="hidden" name="mobile_visibility_form" value="1" />
                            <label style="display:block;margin-bottom:6px;">
                                <input type="checkbox" name="mobile_hide_right_pane" id="aieo-nav-mobile-hide-right" <?php checked($mb_hide_right); ?> />
                                <?php esc_html_e('Hide the second pane (item details + Top Needs + Top Brands)', 'ai-eshop-optimizer'); ?>
                            </label>
                            <label style="display:block;margin-bottom:6px;">
                                <input type="checkbox" name="mobile_hide_top_needs" id="aieo-nav-mobile-hide-needs" <?php checked($mb_hide_needs); ?> />
                                <?php esc_html_e('Hide "Top Needs" highlights only (keeps the rest of the second pane)', 'ai-eshop-optimizer'); ?>
                            </label>
                            <label style="display:block;margin-bottom:6px;">
                                <input type="checkbox" name="mobile_hide_top_brands" id="aieo-nav-mobile-hide-brands" <?php checked($mb_hide_brands); ?> />
                                <?php esc_html_e('Hide "Top Brands" highlights only', 'ai-eshop-optimizer'); ?>
                            </label>
                            <label style="display:block;margin-top:10px;">
                                <?php esc_html_e('Mobile breakpoint (px):', 'ai-eshop-optimizer'); ?>
                                <input type="number" name="mobile_breakpoint" min="320" max="1280" step="1" value="<?php echo (int) $mb_breakpoint; ?>" class="small-text" style="width:80px;" />
                            </label>
                            <p class="description" style="margin-top:6px;">
                                <?php esc_html_e('Rules apply via @media (max-width: …px). Default 768px matches the standard mobile/tablet boundary used elsewhere in the navigator CSS. Set 0 / blank values stay off — default is everything visible on mobile.', 'ai-eshop-optimizer'); ?>
                            </p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><?php esc_html_e('Daily auto-regenerate', 'ai-eshop-optimizer'); ?></th>
                        <td>
                            <label style="margin-right:14px;">
                                <input type="checkbox" id="aieo-navigator-daily-enabled" <?php checked($daily_enabled); ?> />
                                <?php esc_html_e('Enabled', 'ai-eshop-optimizer'); ?>
                            </label>
                            <label>
                                <?php esc_html_e('Time:', 'ai-eshop-optimizer'); ?>
                                <input type="time" id="aieo-navigator-daily-time" value="<?php echo esc_attr($daily_time); ?>" step="60" />
                            </label>
                            <p class="description">
                                <?php
                                printf(
                                    
                                    esc_html__('Site timezone. Next scheduled run: %s', 'ai-eshop-optimizer'),
                                    '<code>' . esc_html($next_run_label) . '</code>'
                                );
                                ?>
                            </p>
                        </td>
                    </tr>
                    <tr>
                        <th scope="row"><?php esc_html_e('Manual regenerate', 'ai-eshop-optimizer'); ?></th>
                        <td>
                            <button type="button" class="button button-primary" id="aieo-navigator-regen">
                                <?php esc_html_e('Regenerate Now', 'ai-eshop-optimizer'); ?>
                            </button>
                            <span class="aieo-navigator-regen-status" style="margin-left:8px;"></span>
                        </td>
                    </tr>
                </tbody>
            </table>

            <script>
            (function () {
                var enabledEl       = document.getElementById('aieo-navigator-enabled');
                var excludedEl      = document.getElementById('aieo-navigator-excluded');
                var dailyEnabledEl  = document.getElementById('aieo-navigator-daily-enabled');
                var dailyTimeEl     = document.getElementById('aieo-navigator-daily-time');
                var regenBtn        = document.getElementById('aieo-navigator-regen');
                var statusEl        = document.querySelector('.aieo-navigator-regen-status');
                // Render nonce directly via PHP — `aieoDmm` global is
                // localized in wp_footer which runs AFTER this inline
                // script, so reading it here would capture an empty string.
                var nonce = <?php echo wp_json_encode( wp_create_nonce( AIEO_DMM::NONCE_ACTION ) ); ?>;
                var ajaxurl = <?php echo wp_json_encode( admin_url( 'admin-ajax.php' ) ); ?>;
                var blogParentEl = document.getElementById('aieo-nav-blog-parent');
                var groupsListEl = document.getElementById('aieo-nav-groups-list');

                function readGroupsConfig() {
                    if (!groupsListEl) return null;
                    var rows = [];
                    groupsListEl.querySelectorAll('.aieo-nav-groups-item').forEach(function (li) {
                        var slug  = li.getAttribute('data-slug') || '';
                        var sel   = li.querySelector('.aieo-nav-placement');
                        var place = sel ? sel.value : 'main';
                        if (slug) rows.push({ slug: slug, placement: place });
                    });
                    return rows;
                }
                function buildPayload(action) {
                    var fd = new FormData();
                    fd.set('action', action);
                    fd.set('nonce', nonce);
                    if (enabledEl && enabledEl.checked) fd.set('enabled', '1');
                    if (excludedEl) fd.set('excluded_terms', excludedEl.value || '');
                    if (dailyEnabledEl) fd.set('daily_enabled', dailyEnabledEl.checked ? '1' : '0');
                    if (dailyTimeEl && dailyTimeEl.value) fd.set('daily_time', dailyTimeEl.value);
                    if (blogParentEl) fd.set('blog_parent_slug', blogParentEl.value || '');
                    var orderMetricEl = document.getElementById('aieo-nav-order-metric');
                    if (orderMetricEl) fd.set('order_metric', orderMetricEl.value || 'turnover');
                    var needsScopedEl = document.getElementById('aieo-nav-needs-scoped');
                    if (needsScopedEl) fd.set('needs_scoped', needsScopedEl.checked ? '1' : '0');
                    var rows = readGroupsConfig();
                    if (rows) fd.set('groups_config', JSON.stringify(rows));
                    // Visual settings — read fresh each call so wpColorPicker
                    // changes are picked up.
                    var form = document.querySelector('.aieo-dmm-navigator');
                    if (form) {
                        // Send EVERY colour input (read by class, by name) so the
                        // newer colours (selection_bg / hover_bg / count_bg /
                        // count_fg) aren't silently dropped — the old hard-coded
                        // 4-key list never sent them, so they could never be saved.
                        form.querySelectorAll('input.aieo-nav-color').forEach(function (el) {
                            if (el.name) fd.set(el.name, el.value || '');
                        });
                        var countStyleEl = form.querySelector('select[name="count_style"]');
                        if (countStyleEl) fd.set('count_style', countStyleEl.value || 'plain');
                        ['border_width','border_radius','padding'].forEach(function (k) {
                            var el = form.querySelector('input[name="' + k + '"]');
                            if (el) fd.set(k, el.value || '0');
                        });
                        // Mobile visibility — checkboxes only POST when ticked,
                        // so set the form-marker every time and the unchecked
                        // ones will read as "off" on the server side.
                        fd.set('mobile_visibility_form', '1');
                        ['mobile_hide_right_pane','mobile_hide_top_needs','mobile_hide_top_brands'].forEach(function (k) {
                            var el = form.querySelector('input[name="' + k + '"]');
                            if (el && el.checked) fd.set(k, '1');
                        });
                        var mbBp = form.querySelector('input[name="mobile_breakpoint"]');
                        if (mbBp) fd.set('mobile_breakpoint', mbBp.value || '768');
                    }
                    return fd;
                }
                function saveSettings() {
                    return fetch(ajaxurl, {
                        method: 'POST',
                        body: buildPayload('aieo_dmm_navigator_save_settings'),
                        credentials: 'same-origin'
                    });
                }
                if (dailyEnabledEl) dailyEnabledEl.addEventListener('change', saveSettings);
                if (dailyTimeEl) dailyTimeEl.addEventListener('change', saveSettings);

                if (enabledEl) {
                    enabledEl.addEventListener('change', function () {
                        var prev = !enabledEl.checked;
                        enabledEl.disabled = true;
                        fetch(ajaxurl, { method: 'POST', body: buildPayload('aieo_dmm_navigator_save_settings'), credentials: 'same-origin' })
                            .then(function (r) { return r.json().catch(function () { return null; }); })
                            .then(function (j) {
                                if (!j || !j.success) {
                                    enabledEl.checked = prev;
                                    console.warn('[aieo-navigator] save failed', j);
                                } else {
                                    console.info('[aieo-navigator] saved enabled=' + (j.data && j.data.enabled));
                                }
                            })
                            .catch(function (err) {
                                enabledEl.checked = prev;
                                console.error('[aieo-navigator] save error', err);
                            })
                            .finally(function () { enabledEl.disabled = false; });
                    });
                }
                // Auto-save excluded list 600ms after the user stops typing.
                if (excludedEl) {
                    var dt;
                    excludedEl.addEventListener('input', function () {
                        clearTimeout(dt);
                        dt = setTimeout(function () {
                            fetch(ajaxurl, { method: 'POST', body: buildPayload('aieo_dmm_navigator_save_settings'), credentials: 'same-origin' });
                        }, 600);
                    });
                }
                // Wire the sortable list — must wait for jQuery UI sortable
                // which is enqueued for the footer; this script runs in
                // the body so the lib isn't available synchronously. The
                // jQuery-ready callback defers until after DOMContentLoaded,
                // by which point all footer scripts (including sortable)
                // have loaded.
                if (groupsListEl && window.jQuery) {
                    jQuery(function ($) {
                        if (!$.fn.sortable) {
                            console.warn('[aieo-navigator] jQuery UI sortable not loaded — drag disabled');
                            return;
                        }
                        $(groupsListEl).sortable({
                            handle: '.aieo-nav-drag',
                            items: '> .aieo-nav-groups-item',
                            placeholder: 'aieo-nav-groups-placeholder',
                            forcePlaceholderSize: true,
                            tolerance: 'pointer',
                            cursor: 'grabbing',
                            opacity: 0.85,
                            update: function () { saveSettings(); }
                        });
                    });
                    // Per-row placement <select> change handler — wired via
                    // delegation so it survives sortable-induced DOM moves.
                    groupsListEl.addEventListener('change', function (e) {
                        if (e.target && e.target.classList.contains('aieo-nav-placement')) {
                            saveSettings();
                        }
                    });
                }
                if (blogParentEl) {
                    blogParentEl.addEventListener('change', saveSettings);
                }
                var orderMetricEl = document.getElementById('aieo-nav-order-metric');
                if (orderMetricEl) {
                    orderMetricEl.addEventListener('change', saveSettings);
                }
                var needsScopedEl = document.getElementById('aieo-nav-needs-scoped');
                if (needsScopedEl) {
                    needsScopedEl.addEventListener('change', saveSettings);
                }
                // Color pickers + numeric inputs: save on change/blur.
                document.querySelectorAll('.aieo-nav-color, .aieo-nav-num').forEach(function (el) {
                    el.addEventListener('change', saveSettings);
                });
                if (regenBtn) {
                    regenBtn.addEventListener('click', function () {
                        regenBtn.disabled = true;
                        // Save the appearance/visual settings FIRST — Regenerate used to
                        // only rebuild the JSON and silently discarded colour changes —
                        // then regenerate the JSON.
                        statusEl.textContent = '<?php echo esc_js(__('Saving…', 'ai-eshop-optimizer')); ?>';
                        saveSettings()
                            .then(function () {
                                statusEl.textContent = '<?php echo esc_js(__('Regenerating…', 'ai-eshop-optimizer')); ?>';
                                return fetch(ajaxurl, { method: 'POST', body: buildPayload('aieo_dmm_navigator_regenerate'), credentials: 'same-origin' });
                            })
                            .then(function (r) { return r.json(); })
                            .then(function (j) {
                                statusEl.textContent = (j && j.success)
                                    ? '<?php echo esc_js(__('Saved + regenerated — version', 'ai-eshop-optimizer')); ?> ' + (j.data && j.data.version)
                                    : '<?php echo esc_js(__('Failed.', 'ai-eshop-optimizer')); ?>';
                            })
                            .catch(function () { statusEl.textContent = '<?php echo esc_js(__('Failed (network)', 'ai-eshop-optimizer')); ?>'; })
                            .finally(function () { regenBtn.disabled = false; });
                    });
                }
            })();
            </script>

            <?php $this->render_cache_config_footer(); ?>
        </div>
        <?php
    }

    
    private function render_cache_config_footer() {
        $upload   = wp_upload_dir();
        $json_url = trailingslashit($upload['baseurl']) . 'aieo/navigator.json';
        $json_rel = '/wp-content/uploads/aieo/navigator.json';
        ?>
        <hr style="margin:32px 0 18px;border:0;border-top:1px solid #e5e7eb;">
        <div class="aieo-dmm-card" style="background:#f9fafb;border-left:4px solid #2271b1;">
            <h3 style="margin:0 0 6px;"><?php esc_html_e('Cache configuration', 'ai-eshop-optimizer'); ?></h3>
            <p class="description" style="margin:0 0 14px;">
                <?php
                echo wp_kses(
                    sprintf(
                        
                        __('The navigator JSON at <code>%s</code> is regenerated on every term/post/menu change. If a CDN or page-cache holds the old copy, customers see stale categories and broken hierarchical URLs. WP Rocket is excluded automatically; <strong>Cloudflare and Nginx need manual setup</strong> below.', 'ai-eshop-optimizer'),
                        esc_html($json_rel)
                    ),
                    array('code' => array(), 'strong' => array())
                );
                ?>
            </p>

            <details open style="margin-top:14px;">
                <summary style="cursor:pointer;font-weight:600;color:#15803d;">
                    ✓ <?php esc_html_e('WP Rocket — handled automatically', 'ai-eshop-optimizer'); ?>
                </summary>
                <p style="margin:8px 0 0;font-size:13px;color:#374151;">
                    <?php esc_html_e('Filters registered by AIEO add the JSON path to WP Rocket\'s page-cache reject list, CDN-rewrite reject list, and JS minification exclusion. No action needed — verify by clearing WP Rocket cache and confirming the JSON URL still loads with a fresh `?v=` query string.', 'ai-eshop-optimizer'); ?>
                </p>
            </details>

            <details style="margin-top:14px;">
                <summary style="cursor:pointer;font-weight:600;color:#b45309;">
                    ⚠ <?php esc_html_e('Cloudflare — manual rule required', 'ai-eshop-optimizer'); ?>
                </summary>
                <div style="margin:8px 0 0;font-size:13px;color:#374151;">
                    <p><?php esc_html_e('Cloudflare ignores query-string variation for `.json` files by default, so our `?v=` cache-buster doesn\'t reach it. Add a Cache Rule (or legacy Page Rule) that bypasses the cache for the navigator JSON.', 'ai-eshop-optimizer'); ?></p>
                    <p style="margin:8px 0 4px;"><strong><?php esc_html_e('Cache Rule (recommended)', 'ai-eshop-optimizer'); ?></strong> — <?php esc_html_e('Cloudflare dashboard → Rules → Cache Rules → Create rule:', 'ai-eshop-optimizer'); ?></p>
                    <ul style="margin:4px 0 4px 20px;list-style:disc;">
                        <li><code><?php esc_html_e('When incoming requests match', 'ai-eshop-optimizer'); ?></code>: <code>URI Path</code> <strong>contains</strong> <code><?php echo esc_html($json_rel); ?></code></li>
                        <li><code><?php esc_html_e('Then cache eligibility', 'ai-eshop-optimizer'); ?></code>: <strong>Bypass cache</strong></li>
                    </ul>
                    <p style="margin:10px 0 4px;"><strong><?php esc_html_e('Legacy Page Rule', 'ai-eshop-optimizer'); ?></strong> — <?php esc_html_e('if your zone still uses Page Rules:', 'ai-eshop-optimizer'); ?></p>
                    <ul style="margin:4px 0 4px 20px;list-style:disc;">
                        <li><?php esc_html_e('URL pattern', 'ai-eshop-optimizer'); ?>: <code>*your-domain.com<?php echo esc_html($json_rel); ?>*</code></li>
                        <li><?php esc_html_e('Setting', 'ai-eshop-optimizer'); ?>: <strong>Cache Level: Bypass</strong></li>
                    </ul>
                </div>
            </details>

            <details style="margin-top:14px;">
                <summary style="cursor:pointer;font-weight:600;color:#b45309;">
                    ⚠ <?php esc_html_e('Nginx — manual config (only if CF bypass alone isn\'t enough)', 'ai-eshop-optimizer'); ?>
                </summary>
                <div style="margin:8px 0 0;font-size:13px;color:#374151;">
                    <p><?php esc_html_e('The navigator JSON is served by Nginx as a static file — PHP never runs for it, so HTTP cache headers must be set at the server level. Add this `location` block inside the site\'s `server { }` block:', 'ai-eshop-optimizer'); ?></p>
                    <pre style="background:#1f2937;color:#e5e7eb;padding:12px;border-radius:6px;overflow:auto;font-size:12px;line-height:1.5;">location ~* /wp-content/uploads/aieo/navigator\.json$ {
    add_header Cache-Control "no-store, must-revalidate, max-age=0" always;
    add_header Pragma "no-cache" always;
    expires off;
    etag off;
    if_modified_since off;
}</pre>
                    <p><?php esc_html_e('Reload Nginx after editing:', 'ai-eshop-optimizer'); ?> <code>nginx -t &amp;&amp; systemctl reload nginx</code></p>
                </div>
            </details>

            <details style="margin-top:14px;">
                <summary style="cursor:pointer;font-weight:600;"><?php esc_html_e('Verification', 'ai-eshop-optimizer'); ?></summary>
                <div style="margin:8px 0 0;font-size:13px;color:#374151;">
                    <p><?php esc_html_e('After regenerating the navigator and saving the cache rules, run this in the browser console on the site:', 'ai-eshop-optimizer'); ?></p>
                    <pre style="background:#1f2937;color:#e5e7eb;padding:12px;border-radius:6px;overflow:auto;font-size:12px;">fetch('<?php echo esc_html($json_rel); ?>?_=' + Date.now())
  .then(r =&gt; console.log(
    'cf-cache-status:', r.headers.get('cf-cache-status'),
    'last-modified:',   r.headers.get('last-modified')
  ));</pre>
                    <p><?php
                        echo wp_kses(
                            __('Expect <code>cf-cache-status: BYPASS</code> (or <code>DYNAMIC</code>), and <code>last-modified</code> matching the most recent term/post/menu change. If <code>last-modified</code> is older than your last edit, the cache layer is still serving stale data — recheck the CF rule and the Nginx <code>location</code> block.', 'ai-eshop-optimizer'),
                            array('code' => array())
                        );
                    ?></p>
                </div>
            </details>
        </div>
        <?php
    }
}
