<?php

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

class AIEO_DMM_Surveys extends AIEO_DMM_Module_Base {

    const SCHEMA_OPTION  = 'aieo_surveys_schema_version';
    const SCHEMA_VERSION = '4';

    
    const FORM_POST_ORDER     = 18;
    const FORM_POST_DELIVERY  = 20;

    
    const CRON_INGEST_HOOK = 'aieo_surveys_nightly_ingest';

    
    const CRON_ASYNC_HOOK  = 'aieo_surveys_async_run';

    
    const CRON_TASTE_HOOK  = 'aieo_surveys_taste_vectors_tick';

    
    const STATUS_OPTION    = 'aieo_surveys_async_status';

    public function get_id()       { return 'surveys'; }
    public function get_label()    { return __('Customer Intelligence', 'ai-eshop-optimizer'); }
    public function get_priority() { return 72; } 
    public function get_group()    { return 'customer_profiling'; }

    public function is_pro_locked() { return true; }
    public function get_pro_promo() {
        return array(
            'title'       => __('Customer Intelligence Surveys', 'ai-eshop-optimizer'),
            'description' => __('Capture WHY customers buy — first-party signal that feeds the needs taxonomy and the recommendation engine. Each survey response makes the next recommendation, the next ad, and the next newsletter measurably more relevant.', 'ai-eshop-optimizer'),
            'bullets'     => array(
                __('Post-purchase, post-delivery, and lapsed-customer survey flows.', 'ai-eshop-optimizer'),
                __('Responses auto-tagged onto the customer + product so they enrich recommendations.', 'ai-eshop-optimizer'),
                __('No third-party SaaS — your customer data stays inside WooCommerce.', 'ai-eshop-optimizer'),
            ),
        );
    }

    public function register_hooks() {
        add_action('plugins_loaded', array($this, 'maybe_install'), 25);
        add_action('plugins_loaded', array($this, 'maybe_schedule_cron'), 26);

        add_action('wp_ajax_aieo_dmm_surveys_recompute', array($this, 'ajax_recompute'));
        add_action('wp_ajax_aieo_dmm_surveys_ingest',    array($this, 'ajax_ingest'));
        add_action('wp_ajax_aieo_dmm_surveys_status',    array($this, 'ajax_status'));
        add_action('wp_ajax_aieo_dmm_surveys_bootstrap_ci', array($this, 'ajax_bootstrap_ci'));
        add_action('wp_ajax_aieo_dmm_surveys_verify_prices', array($this, 'ajax_verify_prices'));
        add_action('wp_ajax_aieo_dmm_surveys_fix_prices',    array($this, 'ajax_fix_prices'));

        
        
        
        add_action('gform_after_submission', array($this, 'on_gform_submission'), 20, 2);

        
        add_action(self::CRON_INGEST_HOOK, array($this, 'cron_run_nightly'));
        add_action(self::CRON_ASYNC_HOOK,  array($this, 'on_async_run'), 10, 1);
        add_action(self::CRON_TASTE_HOOK,  array($this, 'cron_taste_vectors_tick'));
    }

    public function enqueue_admin_assets() {
        
    }

    
    
    

    
    public function ensure_customer_intelligence_table() {
        global $wpdb;
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';
        if ((string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl_ci)) === $tbl_ci) {
            return true;
        }

        $schema_file = AIEO_DIR . 'includes/dmm/sql/push-schema.php';
        if (!file_exists($schema_file)) { return false; }
        require_once $schema_file;
        if (function_exists('aieo_push_install_schema')) {
            
            delete_option('aieo_push_schema_version');
            aieo_push_install_schema();
        }

        return (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl_ci)) === $tbl_ci;
    }

    public function maybe_install() {
        
        
        $this->ensure_customer_intelligence_table();

        $current = (string) get_option(self::SCHEMA_OPTION, '');
        if ($current === self::SCHEMA_VERSION) {
            return;
        }

        global $wpdb;
        $base = AIEO_DIR . 'includes/dmm/sql/';
        require_once $base . 'surveys-schema.php';
        require_once $base . 'surveys-sprocs.php';

        
        
        
        
        if ($current !== '' && $current !== self::SCHEMA_VERSION) {
            $tbl_sr = $wpdb->prefix . 'aieo_survey_responses';
            $tbl_si = $wpdb->prefix . 'aieo_survey_intelligence';
            $wpdb->query("DROP TABLE IF EXISTS {$tbl_sr}");
            $wpdb->query("DROP TABLE IF EXISTS {$tbl_si}");
        }

        if (function_exists('aieo_surveys_install_schema')) { aieo_surveys_install_schema(); }
        if (function_exists('aieo_surveys_install_sprocs')) { aieo_surveys_install_sprocs(); }
        update_option(self::SCHEMA_OPTION, self::SCHEMA_VERSION, false);
    }

    
    
    

    public function maybe_schedule_cron() {
        if (!wp_next_scheduled(self::CRON_INGEST_HOOK)) {
            $tz   = new \DateTimeZone('Europe/Athens');
            $next = new \DateTime('today 03:00', $tz);
            if ($next->getTimestamp() <= time()) {
                $next->modify('+1 day');
            }
            wp_schedule_event($next->getTimestamp(), 'daily', self::CRON_INGEST_HOOK);
        }
        
        
        if (!wp_next_scheduled(self::CRON_TASTE_HOOK)) {
            wp_schedule_event(time() + 60, 'hourly', self::CRON_TASTE_HOOK);
        }
    }

    
    public function cron_taste_vectors_tick() {
        try {
            $this->compute_taste_vectors_batch(50);
        } catch (\Throwable $e) {
            if (function_exists('aieo_debug_log')) {
                aieo_debug_log('AIEO_Surveys taste-vector tick failed: ' . $e->getMessage());
            }
        }
    }

    
    public function cron_run_nightly() {
        global $wpdb;
        $this->maybe_install();

        $started = microtime(true);
        $wpdb->query('CALL aieo_sp_ingest_survey_responses()');
        $wpdb->query('CALL aieo_sp_extract_survey_intelligence()');
        $this->compute_purchase_metrics();
        $duration = round(microtime(true) - $started, 2);

        if (function_exists('aieo_debug_log')) {
            aieo_debug_log('AIEO_Surveys nightly ingest+extract completed in ' . $duration . 's');
        }
    }

    
    
    

    
    public function on_gform_submission($entry, $form) {
        if (!is_array($entry) || !is_array($form)) { return; }
        $form_id = (int) ($form['id'] ?? 0);
        if ($form_id !== self::FORM_POST_ORDER && $form_id !== self::FORM_POST_DELIVERY) {
            return;
        }
        $this->maybe_install();

        if ($form_id === self::FORM_POST_ORDER) {
            $this->ingest_f18_entry($entry);
        } else {
            $this->ingest_f20_entry($entry);
        }
    }

    
    private function ingest_f18_entry($entry) {
        global $wpdb;
        $tbl_sr = $wpdb->prefix . 'aieo_survey_responses';

        $email      = strtolower(trim((string) ($entry['92'] ?? '')));
        $order_id   = (int) ($entry['15'] ?? 0);
        $customer_id= (int) ($entry['16'] ?? 0);

        
        
        $resolved = $this->resolve_eponymous_for_order($order_id);
        if ($resolved['eponymous_id'] === 0 && $email !== '') {
            $resolved = $this->resolve_eponymous_for_email($email);
        }

        $ux_keys = array('21','22','23','24','25','26','27','29','30','31','33','47');
        $ux_vals = array();
        foreach ($ux_keys as $k) {
            $v = (string) ($entry[$k] ?? '');
            if (preg_match('/^[1-5]$/', $v)) { $ux_vals[] = (int) $v; }
        }
        $ux_avg = !empty($ux_vals) ? round(array_sum($ux_vals) / count($ux_vals), 2) : 0;

        $is_pro = (string) ($entry['51'] ?? '');

        $disc_store  = $this->any_checkbox_set($entry, '44');
        $disc_google = $this->any_checkbox_set($entry, '45');
        $disc_ad     = $this->any_checkbox_set($entry, '73');
        $disc_wom    = $this->any_checkbox_set($entry, '43');

        $row = array(
            'form_id'              => self::FORM_POST_ORDER,
            'gf_entry_id'          => (int) ($entry['id'] ?? 0),
            'order_id'             => $order_id,
            'customer_id'          => $customer_id,
            'eponymous_id'         => (int) $resolved['eponymous_id'],
            'wp_user_id'           => (int) ($resolved['wp_user_id'] ?: $customer_id),
            'email'                => $email,
            'submitted_at'         => (string) ($entry['date_created'] ?? current_time('mysql', true)),
            'f18_nps'              => $this->int_or_zero($entry['18'] ?? ''),
            'f18_satisfaction'     => $this->int_or_zero($entry['7']  ?? ''),
            'f18_site_ux_avg'      => $ux_avg,
            'f18_is_pro'           => ($is_pro === '1' || $is_pro === 'Ναι') ? 1 : 0,
            'f18_priorities_raw'   => (string) ($entry['8'] ?? ''),
            'f18_disc_store'       => $disc_store,
            'f18_disc_google'      => $disc_google,
            'f18_disc_ad'          => $disc_ad,
            'f18_disc_wom'         => $disc_wom,
            'f18_eng_seminar'      => $this->yes_no($entry['35'] ?? ''),
            'f18_eng_blog'         => $this->yes_no($entry['38'] ?? ''),
            'f18_eng_fastfinder'   => $this->yes_no($entry['36'] ?? ''),
            'f18_social_login'     => $this->yes_no($entry['37'] ?? ''),
            'f18_shipping_concern' => $this->yes_no($entry['39'] ?? ''),
            'f18_will_followup'    => $this->yes_no($entry['70'] ?? ''),
            'f18_open_feedback'    => (string) ($entry['12'] ?? ''),
        );
        $this->upsert_response($tbl_sr, $row);
    }

    
    private function ingest_f20_entry($entry) {
        global $wpdb;
        $tbl_sr = $wpdb->prefix . 'aieo_survey_responses';

        $email = strtolower(trim((string) ($entry['17'] ?? '')));
        if ($email === '') { return; }

        $resolved = $this->resolve_eponymous_for_email($email);

        $gift_pref_raw = mb_strtolower((string) ($entry['151'] ?? ''));
        $gift_map = array(
            'πρόσωπο' => 'face',
            'σώμα'    => 'body',
            'μαλλιά'  => 'hair',
            'νύχια'   => 'nails',
            'αρώματα' => 'fragrance',
        );
        $gift_pref = $gift_map[$gift_pref_raw] ?? '';

        $row = array(
            'form_id'             => self::FORM_POST_DELIVERY,
            'gf_entry_id'         => (int) ($entry['id'] ?? 0),
            'eponymous_id'        => (int) $resolved['eponymous_id'],
            'wp_user_id'          => (int) $resolved['wp_user_id'],
            'email'               => $email,
            'submitted_at'        => (string) ($entry['date_created'] ?? current_time('mysql', true)),
            'f20_delivery_speed'  => (string) ($entry['21'] ?? ''),
            'f20_packaging'       => $this->int_or_zero($entry['101'] ?? ''),
            'f20_items_clean'     => $this->yes_no($entry['103'] ?? ''),
            'f20_will_repurchase' => $this->yes_no($entry['109'] ?? ''),
            'f20_gift_pref'       => $gift_pref,
            'f20_hair_color'      => (string) ($entry['123'] ?? ''),
            'f20_eye_color'       => (string) ($entry['124'] ?? ''),
            'f20_skin_type'       => (string) ($entry['125'] ?? ''),
            'f20_silhouette'      => (string) ($entry['126'] ?? ''),
            'f20_height'          => (string) ($entry['128'] ?? ''),
        );
        $this->upsert_response($tbl_sr, $row);
    }

    
    private function upsert_response($tbl, array $row) {
        global $wpdb;
        $cols   = array_keys($row);
        $place  = implode(', ', array_fill(0, count($cols), '%s'));
        $assigns = array();
        foreach ($cols as $c) {
            if ($c === 'form_id' || $c === 'gf_entry_id') { continue; }
            $assigns[] = $c . ' = VALUES(' . $c . ')';
        }
        $sql = "INSERT INTO {$tbl} (" . implode(',', $cols) . ") VALUES ({$place})
                ON DUPLICATE KEY UPDATE " . implode(', ', $assigns) . ", updated_at = UTC_TIMESTAMP()";
        $wpdb->query($wpdb->prepare($sql, ...array_values($row)));
    }

    
    private function resolve_eponymous_for_order($order_id) {
        global $wpdb;
        $order_id = (int) $order_id;
        if ($order_id <= 0) { return array('eponymous_id' => 0, 'wp_user_id' => 0); }

        $tables = $this->discover_order_snapshot_tables();
        foreach ($tables as $t) {
            $epo = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT EponymousID FROM `{$t}` WHERE OrderId = %d AND EponymousID > 0 LIMIT 1",
                $order_id
            ));
            if ($epo > 0) {
                $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';
                $wp_user_id = (int) $wpdb->get_var($wpdb->prepare(
                    "SELECT wp_user_id FROM {$tbl_ci} WHERE eponymous_id = %d LIMIT 1",
                    $epo
                ));
                return array('eponymous_id' => $epo, 'wp_user_id' => $wp_user_id);
            }
        }
        return array('eponymous_id' => 0, 'wp_user_id' => 0);
    }

    private function resolve_eponymous_for_email($email) {
        global $wpdb;
        if ($email === '') { return array('eponymous_id' => 0, 'wp_user_id' => 0); }
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';
        
        $row = $wpdb->get_row($wpdb->prepare(
            "SELECT eponymous_id, wp_user_id
             FROM {$tbl_ci}
             WHERE LOWER(TRIM(email)) COLLATE utf8mb4_unicode_ci
                 = %s COLLATE utf8mb4_unicode_ci
             LIMIT 1",
            $email
        ), ARRAY_A);
        if (!$row) { return array('eponymous_id' => 0, 'wp_user_id' => 0); }
        return array(
            'eponymous_id' => (int) $row['eponymous_id'],
            'wp_user_id'   => (int) $row['wp_user_id'],
        );
    }

    private function int_or_zero($v) {
        $v = (string) $v;
        return ($v !== '' && ctype_digit($v)) ? (int) $v : 0;
    }

    private function yes_no($v) {
        $v = (string) $v;
        return ($v === '1' || $v === 'Ναι') ? 1 : 0;
    }

    
    private function any_checkbox_set(array $entry, $base_id) {
        foreach ($entry as $k => $v) {
            if ($v === '' || $v === null) { continue; }
            if ($k === $base_id || strpos((string) $k, $base_id . '.') === 0) {
                return 1;
            }
        }
        return 0;
    }

    
    
    
    
    
    
    
    
    
    
    
    
    

    
    private function discover_order_snapshot_tables() {
        global $wpdb;
        $like = $wpdb->esc_like($wpdb->prefix . 'aieo_') . '%' . $wpdb->esc_like('_orders');
        $found = $wpdb->get_col($wpdb->prepare('SHOW TABLES LIKE %s', $like));
        $monthly_re = '/^' . preg_quote($wpdb->prefix, '/') . 'aieo_\d{4}_\d{2}_orders$/';
        $valid = array();
        foreach ((array) $found as $t) {
            if ($t === $wpdb->prefix . 'aieo_temp_orders' || preg_match($monthly_re, (string) $t)) {
                $valid[] = (string) $t;
            }
        }
        return $valid;
    }

    
    public static function priority_taxonomy() {
        return array(
            'price'           => 'Τιμές Προϊόντων',
            'quality'         => 'Ποιότητα προϊόντων',
            'shipping_cost'   => 'Κόστος αποστολής',
            'know_products'   => 'Να ξέρω τα προϊόντα',
            'store_trust'     => 'Αξιοπιστία καταστήματος',
            'service_speed'   => 'Ταχύτητα εξυπηρέτησης',
            'support'         => 'Υποστήριξη στις ερωτήσεις μου',
            'site_quality'    => 'Ταχύτητα - Ποιότητα ιστοσελίδας',
            'physical_store'  => 'Θετική εμπειρία στο φυσικό κατάστημα',
            'general'         => 'Γενικά (no clear signal)',
        );
    }

    
    public static function get_audience_priority_distribution( $audience_key = 'all' ) {
        global $wpdb;
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';
        
        $tbl_um = $wpdb->usermeta;

        
        $col_exists = (string) $wpdb->get_var($wpdb->prepare(
            "SELECT COLUMN_NAME FROM information_schema.COLUMNS
             WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = %s LIMIT 1",
            $tbl_ci,
            'inferred_top_priority'
        ));
        if ($col_exists === '') {
            return array(
                'totals'       => array(),
                'by_source'    => array(),
                'audience_key' => (string) $audience_key,
                'error'        => 'inferred_priorities columns not yet installed',
            );
        }

        $where = '';
        $args  = array();
        if ($audience_key !== 'all' && $audience_key !== '') {
            
            
            $where = " INNER JOIN {$tbl_um} um
                            ON um.user_id = ci.wp_user_id
                           AND um.meta_key = %s AND um.meta_value = %s ";
            $args[] = class_exists('AIEO_Identity_Bridge')
                ? \AIEO_Identity_Bridge::META_AUDIENCE
                : 'aieo_audience_key';
            $args[] = $audience_key;
        }

        
        $sql = "SELECT
                    ci.inferred_top_priority      AS slug,
                    ci.inferred_priorities_source AS source,
                    COUNT(*)                      AS n,
                    AVG(ci.inferred_priorities_confidence) AS avg_confidence
                  FROM {$tbl_ci} ci
                  {$where}
                  WHERE ci.inferred_top_priority IS NOT NULL
                  GROUP BY ci.inferred_top_priority, ci.inferred_priorities_source";
        $rows = !empty($args)
            ? $wpdb->get_results($wpdb->prepare($sql, $args), ARRAY_A)
            : $wpdb->get_results($sql, ARRAY_A);

        $totals    = array();
        $by_source = array();
        $tax       = self::priority_taxonomy();
        if (is_array($rows)) {
            foreach ($rows as $r) {
                $slug   = (string) $r['slug'];
                $source = (string) $r['source'];
                $n      = (int)   $r['n'];
                $conf   = round((float) $r['avg_confidence'], 2);
                $totals[$slug] = ($totals[$slug] ?? 0) + $n;
                if (!isset($by_source[$source])) { $by_source[$source] = array(); }
                $by_source[$source][] = array(
                    'priority_slug'  => $slug,
                    'priority_label' => $tax[$slug] ?? $slug,
                    'customers'      => $n,
                    'avg_confidence' => $conf,
                );
            }
        }
        
        arsort($totals, SORT_NUMERIC);
        $ranked = array();
        foreach ($totals as $slug => $n) {
            $ranked[] = array(
                'priority_slug'  => $slug,
                'priority_label' => $tax[$slug] ?? $slug,
                'customers'      => (int) $n,
            );
        }

        return array(
            'audience_key' => (string) $audience_key,
            'totals'       => $ranked,
            'by_source'    => $by_source,
        );
    }

    
    public static function get_customer_priority_profile( $user_id ) {
        $user_id = (int) $user_id;
        if ($user_id <= 0) { return null; }
        global $wpdb;
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';
        $row = $wpdb->get_row($wpdb->prepare(
            "SELECT inferred_top_priority, inferred_priorities_json,
                    inferred_priorities_confidence, inferred_priorities_source,
                    inferred_priorities_updated_at, price_band, discount_share,
                    top_brand_id, top_brand_share, segments_json, eponymous_id
               FROM {$tbl_ci}
              WHERE wp_user_id = %d
              ORDER BY updated_at DESC LIMIT 1",
            $user_id
        ), ARRAY_A);
        if (!$row) { return null; }
        $tax = self::priority_taxonomy();
        $slug = (string) $row['inferred_top_priority'];
        return array(
            'customer_id'        => $user_id,
            'pseudonym'          => 'Customer #' . $user_id,
            'priority_slug'      => $slug,
            'priority_label'     => $tax[$slug] ?? $slug,
            'confidence'         => (float) $row['inferred_priorities_confidence'],
            'source'             => (string) $row['inferred_priorities_source'],
            'updated_at'         => (string) $row['inferred_priorities_updated_at'],
            'priorities_json'    => (string) $row['inferred_priorities_json'],
            'price_band'         => isset($row['price_band']) ? (string) $row['price_band'] : '',
            'discount_share'     => isset($row['discount_share']) ? (float) $row['discount_share'] : 0.0,
            'top_brand_id'       => (int) $row['top_brand_id'],
            'top_brand_share'    => isset($row['top_brand_share']) ? (float) $row['top_brand_share'] : 0.0,
            'segments_json'      => (string) ($row['segments_json'] ?? ''),
        );
    }

    
    public static function find_customer_lookalikes( $user_id, $n = 10 ) {
        $user_id = (int) $user_id;
        $n       = max(1, min(50, (int) $n));
        if ($user_id <= 0) { return array(); }
        global $wpdb;
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';

        
        $anchor = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$tbl_ci} WHERE wp_user_id = %d LIMIT 1",
            $user_id
        ), ARRAY_A);
        if (!$anchor) { return array(); }

        
        
        
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT * FROM {$tbl_ci}
              WHERE wp_user_id <> %d
              ORDER BY updated_at DESC
              LIMIT 5000",
            $user_id
        ), ARRAY_A);
        if (empty($rows)) { return array(); }

        $instance = self::get_instance_or_null();
        if ($instance === null) { return array(); }

        $anchor_vec = $instance->build_knn_feature_vector($anchor);
        $distances = array();
        foreach ($rows as $r) {
            $vec = $instance->build_knn_feature_vector($r);
            $d = 0.0;
            foreach ($anchor_vec as $k => $v) {
                $other = $vec[$k] ?? 0.0;
                $d += ($v - $other) * ($v - $other);
            }
            $distances[(int) $r['wp_user_id']] = sqrt($d);
        }
        asort($distances, SORT_NUMERIC);
        $out = array();
        $i = 0;
        foreach ($distances as $uid => $dist) {
            $out[] = array(
                'user_id'   => (int) $uid,
                'pseudonym' => 'Customer #' . (int) $uid,
                'distance'  => round((float) $dist, 4),
            );
            if (++$i >= $n) { break; }
        }
        return $out;
    }

    
    private static function get_instance_or_null() {
        if (!class_exists('AIEO_DMM')) { return null; }
        $dmm = AIEO_DMM::instance();
        if (!$dmm)                       { return null; }
        $mod = $dmm->get_module('surveys');
        return $mod instanceof self ? $mod : null;
    }

    
    public function compute_inferred_priorities_heuristic() {
        global $wpdb;
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';

        
        
        
        $col_exists = (string) $wpdb->get_var($wpdb->prepare(
            "SELECT COLUMN_NAME FROM information_schema.COLUMNS
             WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = %s
             LIMIT 1",
            $tbl_ci,
            'inferred_top_priority'
        ));
        if ($col_exists === '') {
            if (function_exists('aieo_debug_log')) {
                aieo_debug_log('AIEO_Surveys heuristic: inferred_top_priority column missing on ' . $tbl_ci . ' — schema v7 install likely did not fire. Check Web Push module maybe_install.');
            }
            
            delete_option('aieo_push_schema_version');
            if (class_exists('AIEO_DMM_Web_Push') && method_exists('AIEO_DMM_Web_Push', 'instance')) {
                $wp = AIEO_DMM_Web_Push::instance();
                if (method_exists($wp, 'maybe_install')) {
                    $wp->maybe_install();
                }
            }
            
            $col_exists = (string) $wpdb->get_var($wpdb->prepare(
                "SELECT COLUMN_NAME FROM information_schema.COLUMNS
                 WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = %s
                 LIMIT 1",
                $tbl_ci,
                'inferred_top_priority'
            ));
            if ($col_exists === '') {
                if (function_exists('aieo_debug_log')) {
                    aieo_debug_log('AIEO_Surveys heuristic: column STILL missing after forced re-install. Manual ALTER needed.');
                }
                return false;
            }
        }

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        $sql = "UPDATE {$tbl_ci} ci
        SET
            ci.inferred_top_priority = CASE
                WHEN JSON_SEARCH(IFNULL(segments_json,'[]'), 'one', 'newbie')        IS NOT NULL THEN 'know_products'
                WHEN JSON_SEARCH(IFNULL(segments_json,'[]'), 'one', 'replenisher')   IS NOT NULL THEN 'know_products'
                WHEN JSON_SEARCH(IFNULL(segments_json,'[]'), 'one', 'loyalist')      IS NOT NULL THEN 'store_trust'
                WHEN JSON_SEARCH(IFNULL(segments_json,'[]'), 'one', 'brand_devoted') IS NOT NULL THEN 'store_trust'
                WHEN JSON_SEARCH(IFNULL(segments_json,'[]'), 'one', 'vip')           IS NOT NULL THEN 'quality'
                WHEN aov > 0  AND aov < 30 AND orders_count >= 2 THEN 'price'
                WHEN aov >= 60             AND orders_count >= 2 THEN 'quality'
                ELSE 'general'
            END,
            ci.inferred_priorities_confidence = CASE
                WHEN JSON_SEARCH(IFNULL(segments_json,'[]'), 'one', 'newbie')        IS NOT NULL THEN 0.90
                WHEN JSON_SEARCH(IFNULL(segments_json,'[]'), 'one', 'replenisher')   IS NOT NULL THEN 0.85
                WHEN JSON_SEARCH(IFNULL(segments_json,'[]'), 'one', 'loyalist')      IS NOT NULL THEN 0.95
                WHEN JSON_SEARCH(IFNULL(segments_json,'[]'), 'one', 'brand_devoted') IS NOT NULL THEN 0.85
                WHEN JSON_SEARCH(IFNULL(segments_json,'[]'), 'one', 'vip')           IS NOT NULL THEN 0.70
                WHEN aov > 0  AND aov < 30 AND orders_count >= 2 THEN 0.55
                WHEN aov >= 60             AND orders_count >= 2 THEN 0.55
                ELSE 0.00
            END,
            ci.inferred_priorities_json = CONCAT(
                '{\"basis\":\"',
                CASE
                    WHEN JSON_SEARCH(IFNULL(segments_json,'[]'), 'one', 'newbie')        IS NOT NULL THEN 'segment:newbie'
                    WHEN JSON_SEARCH(IFNULL(segments_json,'[]'), 'one', 'replenisher')   IS NOT NULL THEN 'segment:replenisher'
                    WHEN JSON_SEARCH(IFNULL(segments_json,'[]'), 'one', 'loyalist')      IS NOT NULL THEN 'segment:loyalist'
                    WHEN JSON_SEARCH(IFNULL(segments_json,'[]'), 'one', 'brand_devoted') IS NOT NULL THEN 'segment:brand_devoted'
                    WHEN JSON_SEARCH(IFNULL(segments_json,'[]'), 'one', 'vip')           IS NOT NULL THEN 'segment:vip'
                    WHEN aov > 0  AND aov < 30 AND orders_count >= 2 THEN 'aov_low_repeat'
                    WHEN aov >= 60             AND orders_count >= 2 THEN 'aov_high_repeat'
                    ELSE 'no_signal'
                END,
                '\"}'
            ),
            ci.inferred_priorities_source = 'heuristic_v3',
            ci.inferred_priorities_updated_at = UTC_TIMESTAMP()";

        $wpdb->query($sql);
        return true;
    }

    
    public function validate_knn_leave_one_out() {
        $cache_key = 'aieo_surveys_knn_loo_eval';
        $cached = get_transient($cache_key);
        if (is_array($cached) && isset($cached['total'])) {
            return $cached;
        }

        global $wpdb;
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';
        $tbl_si = $wpdb->prefix . 'aieo_survey_intelligence';

        $rows = $wpdb->get_results(
            "SELECT ci.eponymous_id, ci.ltv_total, ci.aov, ci.orders_count, ci.recency_days,
                    ci.top_brand_share, ci.monetary_band, ci.frequency_band, ci.recency_band,
                    ci.discount_share, ci.price_band,
                    ci.sole_order_share, ci.avg_jlw, ci.avg_vlw,
                    ci.core_needs_json,
                    ci.emails_sent_lifetime, ci.open_rate_lifetime,
                    ci.click_rate_lifetime, ci.days_since_last_engagement,
                    ci.taste_vector,
                    ci.segments_json, si.personal_priorities
             FROM {$tbl_ci} ci
             JOIN {$tbl_si} si ON si.eponymous_id = ci.eponymous_id
             WHERE si.f18_entry_id > 0
               AND si.personal_priorities IS NOT NULL AND si.personal_priorities <> ''",
            ARRAY_A
        );
        if (empty($rows)) {
            return array('total' => 0, 'top1' => 0, 'top3' => 0, 'top1_pct' => 0, 'top3_pct' => 0);
        }

        $grank_to_label = $this->get_f18_priority_map();
        if (empty($grank_to_label)) {
            return array('total' => 0, 'top1' => 0, 'top3' => 0, 'top1_pct' => 0, 'top3_pct' => 0);
        }
        $label_to_slug = $this->build_label_to_slug_map($grank_to_label);
        $default_csv   = implode(',', array_keys($grank_to_label));

        
        $labeled = array();
        foreach ($rows as $r) {
            $cleaned = preg_replace('/\s+/', '', (string) $r['personal_priorities']);
            if ($cleaned === $default_csv) { continue; }

            $slugs = array();
            foreach (explode(',', (string) $r['personal_priorities']) as $g) {
                $g = trim($g);
                if (!isset($grank_to_label[$g])) { continue; }
                $label = $grank_to_label[$g];
                if (isset($label_to_slug[$label])) { $slugs[] = $label_to_slug[$label]; }
            }
            if (empty($slugs)) { continue; }

            $needs = !empty($r['core_needs_json']) ? json_decode((string) $r['core_needs_json'], true) : array();
            
            
            
            
            $taste_raw  = $this->unpack_taste_vector($r['taste_vector'] ?? null);
            $taste_norm = $taste_raw === null ? null : $this->normalize_vector($taste_raw);
            $labeled[] = array(
                'epo'   => (int) $r['eponymous_id'],
                'vec'   => $this->build_knn_feature_vector($r),
                'needs' => is_array($needs) ? $needs : array(),
                'taste' => $taste_norm,
                'top1'  => $slugs[0],
                'top3'  => array_slice($slugs, 0, 3),
            );
        }

        $n = count($labeled);
        if ($n < 50) {
            return array('total' => 0, 'top1' => 0, 'top3' => 0, 'top1_pct' => 0, 'top3_pct' => 0);
        }

        
        
        
        $LOO_MAX = 400;
        if ($n > $LOO_MAX) {
            $idx = range(0, $n - 1);
            shuffle($idx);
            $sampled = array();
            foreach (array_slice($idx, 0, $LOO_MAX) as $kk) { $sampled[] = $labeled[$kk]; }
            $labeled = $sampled;
            $n = count($labeled);
        }

        $k = min(20, $n - 1);
        $NEEDS_WEIGHT = 3.0; 
        $TASTE_WEIGHT = 4.0;

        $top1_hits = 0;
        $top3_hits = 0;

        
        
        
        
        
        for ($i = 0; $i < $n; $i++) {
            $self = $labeled[$i];
            $self_needs = $self['needs'];
            $self_taste = $self['taste'];
            $dim = count($self['vec']);
            $distances = array();
            for ($j = 0; $j < $n; $j++) {
                if ($j === $i) { continue; }
                $other = $labeled[$j];
                $d = 0.0;
                for ($f = 0; $f < $dim; $f++) {
                    $delta = $self['vec'][$f] - $other['vec'][$f];
                    $d += $delta * $delta;
                }
                if (!empty($self_needs) && !empty($other['needs'])) {
                    $cos = $this->needs_cosine_similarity($self_needs, $other['needs']);
                    $needs_dist = 1.0 - $cos;
                    $d += pow($needs_dist * $NEEDS_WEIGHT, 2);
                }
                if ($self_taste !== null && $other['taste'] !== null) {
                    $tcos = $this->dot_product($self_taste, $other['taste']);
                    $taste_dist = 1.0 - $tcos;
                    $d += pow($taste_dist * $TASTE_WEIGHT, 2);
                }
                $distances[$j] = $d;
            }
            asort($distances);
            $top_idx = array_slice($distances, 0, $k, true);

            $vote = array();
            foreach ($top_idx as $j => $sq_dist) {
                $weight = 1.0 / (1.0 + sqrt($sq_dist));
                foreach ($labeled[$j]['top3'] as $rank_idx => $slug) {
                    $rank_w = (3 - $rank_idx) / 3.0;
                    if (!isset($vote[$slug])) { $vote[$slug] = 0.0; }
                    $vote[$slug] += $weight * $rank_w;
                }
            }
            if (empty($vote)) { continue; }
            arsort($vote);
            $predicted = key($vote);

            if ($predicted === $self['top1']) { $top1_hits++; }
            if (in_array($predicted, $self['top3'], true)) { $top3_hits++; }
        }

        $result = array(
            'total'    => $n,
            'top1'     => $top1_hits,
            'top3'     => $top3_hits,
            'top1_pct' => $n > 0 ? round(100 * $top1_hits / $n, 1) : 0,
            'top3_pct' => $n > 0 ? round(100 * $top3_hits / $n, 1) : 0,
        );
        set_transient($cache_key, $result, DAY_IN_SECONDS);
        return $result;
    }

    
    public function compute_position_signals_from_temp_orders() {
        global $wpdb;
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';

        $snapshots = $this->discover_order_snapshot_tables();
        if (empty($snapshots)) { return false; }

        
        
        
        $parts = array();
        foreach ($snapshots as $t) {
            $parts[] = "SELECT EponymousID, OrderId, ProductId, ItemSequence, OrderSize, "
                     . "JourneyLinearWeight, LineValue, OrderItemsTotal "
                     . "FROM `{$t}` WHERE EponymousID > 0 AND ProductId > 0 AND ItemSequence > 0";
        }
        $union = implode(' UNION ALL ', $parts);

        
        
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_pos_lines");
        $wpdb->query(
            "CREATE TEMPORARY TABLE _aieo_pos_lines (
                EponymousID BIGINT UNSIGNED NOT NULL,
                OrderId BIGINT UNSIGNED NOT NULL,
                ProductId BIGINT UNSIGNED NOT NULL,
                ItemSequence INT UNSIGNED NOT NULL,
                OrderSize INT UNSIGNED NOT NULL,
                JourneyLinearWeight DECIMAL(10,6) NULL,
                LineValue DECIMAL(12,4) NULL,
                OrderItemsTotal DECIMAL(12,4) NULL,
                KEY idx_epo (EponymousID),
                KEY idx_epo_prod (EponymousID, ProductId)
             ) ENGINE=MEMORY"
        );
        $wpdb->query("INSERT INTO _aieo_pos_lines SELECT * FROM ({$union}) raw");

        
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_pos_scalars");
        $wpdb->query(
            "CREATE TEMPORARY TABLE _aieo_pos_scalars (
                EponymousID BIGINT UNSIGNED PRIMARY KEY,
                total_orders INT UNSIGNED NOT NULL DEFAULT 0,
                sole_orders INT UNSIGNED NOT NULL DEFAULT 0,
                avg_jlw DECIMAL(5,4) NOT NULL DEFAULT 0,
                avg_vlw DECIMAL(5,4) NOT NULL DEFAULT 0
             ) ENGINE=MEMORY"
        );
        $wpdb->query(
            "INSERT INTO _aieo_pos_scalars (EponymousID, total_orders, sole_orders, avg_jlw, avg_vlw)
             SELECT
                 EponymousID,
                 COUNT(DISTINCT OrderId) AS total_orders,
                 COUNT(DISTINCT IF(OrderSize = 1, OrderId, NULL)) AS sole_orders,
                 ROUND(AVG(IFNULL(JourneyLinearWeight, ItemSequence / NULLIF(OrderSize, 0))), 4) AS avg_jlw,
                 ROUND(AVG(LineValue / NULLIF(OrderItemsTotal, 0)), 4) AS avg_vlw
             FROM _aieo_pos_lines
             GROUP BY EponymousID"
        );

        
        
        
        
        
        
        
        
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_pos_top");
        $wpdb->query(
            "CREATE TEMPORARY TABLE _aieo_pos_top (
                EponymousID BIGINT UNSIGNED PRIMARY KEY,
                top_sole_product_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
                top_first_product_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
                top_last_product_id BIGINT UNSIGNED NOT NULL DEFAULT 0
             ) ENGINE=MEMORY"
        );

        
        
        $wpdb->query(
            "INSERT INTO _aieo_pos_top (EponymousID, top_sole_product_id, top_first_product_id, top_last_product_id)
             SELECT
                 EponymousID,
                 MAX(IF(class = 'sole'  AND rn = 1, ProductId, 0)) AS top_sole_product_id,
                 MAX(IF(class = 'first' AND rn = 1, ProductId, 0)) AS top_first_product_id,
                 MAX(IF(class = 'last'  AND rn = 1, ProductId, 0)) AS top_last_product_id
             FROM (
                 SELECT
                     EponymousID, ProductId, class, hits,
                     ROW_NUMBER() OVER (PARTITION BY EponymousID, class ORDER BY hits DESC, ProductId ASC) AS rn
                 FROM (
                     SELECT EponymousID, ProductId, 'sole' AS class, COUNT(*) AS hits
                     FROM _aieo_pos_lines
                     WHERE OrderSize = 1
                     GROUP BY EponymousID, ProductId
                     UNION ALL
                     SELECT EponymousID, ProductId, 'first' AS class, COUNT(*) AS hits
                     FROM _aieo_pos_lines
                     WHERE ItemSequence = 1 AND OrderSize > 1
                     GROUP BY EponymousID, ProductId
                     UNION ALL
                     SELECT EponymousID, ProductId, 'last' AS class, COUNT(*) AS hits
                     FROM _aieo_pos_lines
                     WHERE ItemSequence = OrderSize AND OrderSize > 1
                     GROUP BY EponymousID, ProductId
                 ) class_counts
             ) ranked
             WHERE rn = 1
             GROUP BY EponymousID"
        );

        
        
        $wpdb->query(
            "UPDATE {$tbl_ci} ci
             LEFT JOIN _aieo_pos_scalars ps ON ps.EponymousID = ci.eponymous_id
             LEFT JOIN _aieo_pos_top    pt ON pt.EponymousID = ci.eponymous_id
             SET
                 ci.sole_order_share     = IFNULL(ps.sole_orders / NULLIF(ps.total_orders, 0), 0),
                 ci.avg_jlw              = IFNULL(ps.avg_jlw, 0),
                 ci.avg_vlw              = IFNULL(ps.avg_vlw, 0),
                 ci.top_sole_product_id  = IFNULL(pt.top_sole_product_id, 0),
                 ci.top_first_product_id = IFNULL(pt.top_first_product_id, 0),
                 ci.top_last_product_id  = IFNULL(pt.top_last_product_id, 0)"
        );

        
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_pos_lines");
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_pos_scalars");
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_pos_top");

        return array(
            'with_sole_share' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_ci} WHERE sole_order_share > 0"),
            'pure_intent_customers' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_ci} WHERE sole_order_share >= 0.8"),
            'avg_sole_share' => (float) $wpdb->get_var("SELECT ROUND(AVG(sole_order_share), 4) FROM {$tbl_ci} WHERE sole_order_share > 0"),
        );
    }

    
    public function compute_tnp_engagement_signals() {
        global $wpdb;
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';

        
        $tnp = $wpdb->prefix . 'newsletter';
        $exists = (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tnp)) === $tnp;
        if (!$exists) {
            return array('skipped' => 'tnp_not_installed');
        }

        
        
        
        
        $has_subs   = (int) $wpdb->get_var("SELECT 1 FROM {$tnp} LIMIT 1");
        $has_emails = (int) $wpdb->get_var("SELECT 1 FROM {$tbl_ci} WHERE email <> '' LIMIT 1");
        if (!$has_subs || !$has_emails) {
            return array('skipped' => 'no_data', 'has_subs' => $has_subs, 'has_emails' => $has_emails);
        }

        
        
        
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_tnp_bridge");
        $wpdb->query(
            "CREATE TEMPORARY TABLE _aieo_tnp_bridge (
                eponymous_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
                tnp_subscriber_id INT UNSIGNED NOT NULL,
                tnp_status CHAR(1) NOT NULL,
                KEY idx_subscriber (tnp_subscriber_id)
             ) ENGINE=MEMORY"
        );
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        $tnp_email_collation = (string) $wpdb->get_var($wpdb->prepare(
            "SELECT COLLATION_NAME FROM information_schema.COLUMNS
             WHERE TABLE_SCHEMA = DATABASE()
               AND TABLE_NAME = %s
               AND COLUMN_NAME = 'email'",
            $tnp
        ));
        if ($tnp_email_collation === '') { $tnp_email_collation = 'utf8mb4_unicode_520_ci'; }
        
        $tnp_email_collation = preg_replace('/[^a-z0-9_]/i', '', $tnp_email_collation);

        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_ci_emails");
        $wpdb->query(
            "CREATE TEMPORARY TABLE _aieo_ci_emails (
                eponymous_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
                email VARCHAR(190) NOT NULL COLLATE {$tnp_email_collation},
                KEY idx_email (email)
             ) ENGINE=MEMORY"
        );
        $wpdb->query(
            "INSERT INTO _aieo_ci_emails (eponymous_id, email)
             SELECT eponymous_id, email FROM {$tbl_ci} WHERE email <> ''"
        );
        $wpdb->query(
            "INSERT INTO _aieo_tnp_bridge (eponymous_id, tnp_subscriber_id, tnp_status)
             SELECT c.eponymous_id, n.id, n.status
             FROM _aieo_ci_emails c
             JOIN {$wpdb->prefix}newsletter n ON n.email = c.email
             ON DUPLICATE KEY UPDATE
                tnp_subscriber_id = VALUES(tnp_subscriber_id),
                tnp_status = VALUES(tnp_status)"
        );
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_ci_emails");

        
        
        
        $thirty_days_ago_unix = time() - (30 * 86400);

        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_tnp_sent_agg");
        $wpdb->query(
            "CREATE TEMPORARY TABLE _aieo_tnp_sent_agg (
                tnp_subscriber_id INT UNSIGNED NOT NULL PRIMARY KEY,
                sent_lifetime INT UNSIGNED NOT NULL DEFAULT 0,
                opened_lifetime INT UNSIGNED NOT NULL DEFAULT 0,
                sent_30d SMALLINT UNSIGNED NOT NULL DEFAULT 0,
                opened_30d SMALLINT UNSIGNED NOT NULL DEFAULT 0,
                last_open_unix INT UNSIGNED NOT NULL DEFAULT 0
             ) ENGINE=MEMORY"
        );
        
        
        $wpdb->query($wpdb->prepare(
            "INSERT INTO _aieo_tnp_sent_agg
                (tnp_subscriber_id, sent_lifetime, opened_lifetime, sent_30d, opened_30d, last_open_unix)
             SELECT
                 s.user_id,
                 COUNT(*) AS sent_lifetime,
                 SUM(IF(s.open > 0, 1, 0)) AS opened_lifetime,
                 SUM(IF(s.time >= %d, 1, 0)) AS sent_30d,
                 SUM(IF(s.time >= %d AND s.open > 0, 1, 0)) AS opened_30d,
                 MAX(IF(s.open > 0, s.time, 0)) AS last_open_unix
             FROM {$wpdb->prefix}newsletter_sent s
             JOIN _aieo_tnp_bridge b ON b.tnp_subscriber_id = s.user_id
             GROUP BY s.user_id",
            $thirty_days_ago_unix,
            $thirty_days_ago_unix
        ));

        
        
        
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_tnp_click_agg");
        $wpdb->query(
            "CREATE TEMPORARY TABLE _aieo_tnp_click_agg (
                tnp_subscriber_id INT UNSIGNED NOT NULL PRIMARY KEY,
                clicked_lifetime INT UNSIGNED NOT NULL DEFAULT 0,
                last_click_at DATETIME NULL
             ) ENGINE=MEMORY"
        );
        $wpdb->query(
            "INSERT INTO _aieo_tnp_click_agg (tnp_subscriber_id, clicked_lifetime, last_click_at)
             SELECT
                 st.user_id,
                 COUNT(DISTINCT st.email_id) AS clicked_lifetime,
                 MAX(st.created) AS last_click_at
             FROM {$wpdb->prefix}newsletter_stats st
             JOIN _aieo_tnp_bridge b ON b.tnp_subscriber_id = st.user_id
             WHERE st.user_id > 0
             GROUP BY st.user_id"
        );

        
        
        
        
        $wpdb->query(
            "UPDATE {$tbl_ci} ci
             LEFT JOIN _aieo_tnp_bridge    b  ON b.eponymous_id      = ci.eponymous_id
             LEFT JOIN _aieo_tnp_sent_agg  sa ON sa.tnp_subscriber_id = b.tnp_subscriber_id
             LEFT JOIN _aieo_tnp_click_agg ca ON ca.tnp_subscriber_id = b.tnp_subscriber_id
             SET
                 ci.tnp_subscriber_id      = IFNULL(b.tnp_subscriber_id, 0),
                 ci.tnp_status             = IFNULL(b.tnp_status, ''),
                 ci.emails_sent_lifetime   = IFNULL(sa.sent_lifetime, 0),
                 ci.emails_opened_lifetime = IFNULL(sa.opened_lifetime, 0),
                 ci.emails_clicked_lifetime= IFNULL(ca.clicked_lifetime, 0),
                 ci.emails_sent_30d        = IFNULL(sa.sent_30d, 0),
                 ci.emails_opened_30d      = IFNULL(sa.opened_30d, 0),
                 ci.open_rate_lifetime     = IFNULL(ROUND(sa.opened_lifetime / NULLIF(sa.sent_lifetime, 0), 4), 0),
                 ci.open_rate_30d          = IFNULL(ROUND(sa.opened_30d      / NULLIF(sa.sent_30d, 0), 4), 0),
                 ci.click_rate_lifetime    = IFNULL(ROUND(ca.clicked_lifetime / NULLIF(sa.sent_lifetime, 0), 4), 0),
                 ci.last_email_open_at     = IF(IFNULL(sa.last_open_unix, 0) > 0, FROM_UNIXTIME(sa.last_open_unix), NULL),
                 ci.last_email_click_at    = ca.last_click_at,
                 ci.days_since_last_engagement = LEAST(
                    65535,
                    GREATEST(
                        0,
                        DATEDIFF(
                            UTC_TIMESTAMP(),
                            GREATEST(
                                IF(IFNULL(sa.last_open_unix, 0) > 0, FROM_UNIXTIME(sa.last_open_unix), '1970-01-01'),
                                IFNULL(ca.last_click_at, '1970-01-01')
                            )
                        )
                    )
                 )"
        );

        
        
        $wpdb->query(
            "UPDATE {$tbl_ci}
             SET days_since_last_engagement = 65535
             WHERE last_email_open_at IS NULL AND last_email_click_at IS NULL"
        );

        
        
        
        $stats = array(
            'matched_subscribers' => (int) $wpdb->get_var("SELECT COUNT(DISTINCT email) FROM {$tbl_ci} WHERE email <> '' AND tnp_subscriber_id > 0"),
            'engaged_lifetime'    => (int) $wpdb->get_var("SELECT COUNT(DISTINCT email) FROM {$tbl_ci} WHERE email <> '' AND emails_opened_lifetime > 0"),
            'engaged_30d'         => (int) $wpdb->get_var("SELECT COUNT(DISTINCT email) FROM {$tbl_ci} WHERE email <> '' AND emails_opened_30d > 0"),
            'avg_open_rate_lifetime' => (float) $wpdb->get_var("SELECT ROUND(AVG(open_rate_lifetime), 4) FROM {$tbl_ci} WHERE emails_sent_lifetime >= 3"),
            'avg_click_rate_lifetime' => (float) $wpdb->get_var("SELECT ROUND(AVG(click_rate_lifetime), 4) FROM {$tbl_ci} WHERE emails_sent_lifetime >= 3"),
        );

        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_tnp_click_agg");
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_tnp_sent_agg");
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_tnp_bridge");

        return $stats;
    }

    
    public function compute_core_needs_from_top_products() {
        global $wpdb;
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';
        $tbl_tr = $wpdb->prefix . 'term_relationships';
        $tbl_tt = $wpdb->prefix . 'term_taxonomy';

        
        
        $has_pa_need = (int) $wpdb->get_var(
            $wpdb->prepare("SELECT COUNT(*) FROM {$tbl_tt} WHERE taxonomy = %s", 'pa_need')
        );
        if ($has_pa_need === 0) { return false; }

        
        
        
        
        
        
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_core_needs");
        $wpdb->query(
            "CREATE TEMPORARY TABLE _aieo_core_needs (
                eponymous_id BIGINT UNSIGNED NOT NULL,
                need_term_id BIGINT UNSIGNED NOT NULL,
                weight TINYINT UNSIGNED NOT NULL,
                PRIMARY KEY (eponymous_id, need_term_id),
                KEY idx_epo (eponymous_id)
             ) ENGINE=MEMORY"
        );

        
        
        
        foreach (array(
            array('column' => 'top_sole_product_id',  'weight' => 3),
            array('column' => 'top_first_product_id', 'weight' => 2),
            array('column' => 'top_last_product_id',  'weight' => 1),
        ) as $pass) {
            $wpdb->query(
                "INSERT INTO _aieo_core_needs (eponymous_id, need_term_id, weight)
                 SELECT ci.eponymous_id, tt.term_id, %d
                 FROM {$tbl_ci} ci
                 JOIN {$tbl_tr} tr ON tr.object_id = ci.{$pass['column']}
                 JOIN {$tbl_tt} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = 'pa_need'
                 WHERE ci.{$pass['column']} > 0
                 ON DUPLICATE KEY UPDATE weight = weight + %d"
            ); 
        }
        
        
        $wpdb->query("DELETE FROM _aieo_core_needs");
        foreach (array(
            array('column' => 'top_sole_product_id',  'weight' => 3),
            array('column' => 'top_first_product_id', 'weight' => 2),
            array('column' => 'top_last_product_id',  'weight' => 1),
        ) as $pass) {
            $wpdb->query($wpdb->prepare(
                "INSERT INTO _aieo_core_needs (eponymous_id, need_term_id, weight)
                 SELECT ci.eponymous_id, tt.term_id, %d
                 FROM {$tbl_ci} ci
                 JOIN {$tbl_tr} tr ON tr.object_id = ci.{$pass['column']}
                 JOIN {$tbl_tt} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id AND tt.taxonomy = 'pa_need'
                 WHERE ci.{$pass['column']} > 0
                 ON DUPLICATE KEY UPDATE weight = weight + %d",
                $pass['weight'],
                $pass['weight']
            ));
        }

        
        
        $wpdb->query(
            "UPDATE {$tbl_ci} ci
             LEFT JOIN (
                 SELECT
                     eponymous_id,
                     CONCAT('{',
                            GROUP_CONCAT(CONCAT('\"', need_term_id, '\":', weight) ORDER BY weight DESC, need_term_id ASC SEPARATOR ','),
                            '}') AS json
                 FROM _aieo_core_needs
                 GROUP BY eponymous_id
             ) cn ON cn.eponymous_id = ci.eponymous_id
             SET ci.core_needs_json = IFNULL(cn.json, NULL)"
        );

        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_core_needs");

        return array(
            'customers_with_needs' => (int) $wpdb->get_var(
                "SELECT COUNT(*) FROM {$tbl_ci} WHERE core_needs_json IS NOT NULL AND core_needs_json <> ''"
            ),
        );
    }

    
    public function compute_discount_and_price_from_wc() {
        global $wpdb;
        $tbl_ci  = $wpdb->prefix . 'aieo_customer_intelligence';
        $tbl_oi  = $wpdb->prefix . 'woocommerce_order_items';
        $tbl_oim = $wpdb->prefix . 'woocommerce_order_itemmeta';

        
        
        if ((string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl_oi)) !== $tbl_oi) {
            return false;
        }

        $snapshots = $this->discover_order_snapshot_tables();
        if (empty($snapshots)) { return false; }

        
        
        
        $parts = array();
        foreach ($snapshots as $t) {
            $parts[] = "SELECT DISTINCT OrderId, EponymousID FROM `{$t}` WHERE OrderId > 0 AND EponymousID IS NOT NULL AND EponymousID > 0";
        }
        $union = implode(' UNION ALL ', $parts);

        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_epo_orders");
        $wpdb->query(
            "CREATE TEMPORARY TABLE _aieo_epo_orders (
                OrderId BIGINT UNSIGNED NOT NULL,
                EponymousID BIGINT UNSIGNED NOT NULL,
                PRIMARY KEY (OrderId, EponymousID),
                KEY idx_epo (EponymousID)
             ) ENGINE=MEMORY"
        );
        $wpdb->query(
            "INSERT IGNORE INTO _aieo_epo_orders (OrderId, EponymousID)
             SELECT OrderId, EponymousID FROM ({$union}) raw"
        );

        
        
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_wc_prices");
        $wpdb->query(
            "CREATE TEMPORARY TABLE _aieo_wc_prices (
                EponymousID BIGINT UNSIGNED NOT NULL PRIMARY KEY,
                discounted_lines INT UNSIGNED NOT NULL DEFAULT 0,
                total_lines INT UNSIGNED NOT NULL DEFAULT 0,
                avg_unit_price DECIMAL(10,2) NOT NULL DEFAULT 0
             ) ENGINE=MEMORY"
        );
        $wpdb->query(
            "INSERT INTO _aieo_wc_prices (EponymousID, discounted_lines, total_lines, avg_unit_price)
             SELECT
                 ep.EponymousID,
                 SUM(IF(
                     CAST(IFNULL(oim_lt.meta_value, '0') AS DECIMAL(12,4))
                     <
                     CAST(IFNULL(oim_ls.meta_value, '0') AS DECIMAL(12,4))
                     AND CAST(IFNULL(oim_ls.meta_value, '0') AS DECIMAL(12,4)) > 0,
                 1, 0)) AS discounted_lines,
                 COUNT(*) AS total_lines,
                 AVG(
                     IF(
                         CAST(IFNULL(oim_q.meta_value, '0') AS DECIMAL(12,4)) > 0
                         AND CAST(IFNULL(oim_ls.meta_value, '0') AS DECIMAL(12,4)) > 0,
                         CAST(oim_ls.meta_value AS DECIMAL(12,4))
                         / CAST(oim_q.meta_value AS DECIMAL(12,4)),
                         NULL
                     )
                 ) AS avg_unit_price
             FROM _aieo_epo_orders ep
             JOIN {$tbl_oi} oi
                 ON oi.order_id = ep.OrderId AND oi.order_item_type = 'line_item'
             LEFT JOIN {$tbl_oim} oim_ls
                 ON oim_ls.order_item_id = oi.order_item_id AND oim_ls.meta_key = '_line_subtotal'
             LEFT JOIN {$tbl_oim} oim_lt
                 ON oim_lt.order_item_id = oi.order_item_id AND oim_lt.meta_key = '_line_total'
             LEFT JOIN {$tbl_oim} oim_q
                 ON oim_q.order_item_id  = oi.order_item_id AND oim_q.meta_key  = '_qty'
             GROUP BY ep.EponymousID"
        );

        
        $wpdb->query(
            "UPDATE {$tbl_ci} ci
             LEFT JOIN _aieo_wc_prices wc ON wc.EponymousID = ci.eponymous_id
             SET
                 ci.discount_share = IFNULL(wc.discounted_lines / NULLIF(wc.total_lines, 0), 0),
                 ci.avg_item_price = IFNULL(wc.avg_unit_price, 0)"
        );

        
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_price_band");
        $wpdb->query(
            "CREATE TEMPORARY TABLE _aieo_price_band (
                eponymous_id BIGINT UNSIGNED PRIMARY KEY,
                pb TINYINT UNSIGNED NOT NULL DEFAULT 0
             ) ENGINE=MEMORY"
        );
        $wpdb->query(
            "INSERT INTO _aieo_price_band (eponymous_id, pb)
             SELECT eponymous_id,
                    NTILE(5) OVER (ORDER BY avg_item_price ASC, eponymous_id ASC)
             FROM {$tbl_ci}
             WHERE avg_item_price > 0"
        );
        $wpdb->query(
            "UPDATE {$tbl_ci} ci
             LEFT JOIN _aieo_price_band pb ON pb.eponymous_id = ci.eponymous_id
             SET ci.price_band = IFNULL(pb.pb, 0)"
        );

        
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_wc_prices");
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_epo_orders");
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_price_band");

        
        
        return array(
            'with_discount_data' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_ci} WHERE discount_share > 0"),
            'with_price_band'    => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_ci} WHERE price_band > 0"),
            'avg_discount_share' => (float) $wpdb->get_var("SELECT ROUND(AVG(discount_share), 4) FROM {$tbl_ci} WHERE discount_share > 0"),
        );
    }

    
    public function compute_inferred_priorities_knn() {
        global $wpdb;
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';
        $tbl_si = $wpdb->prefix . 'aieo_survey_intelligence';
        @set_time_limit(0);

        
        
        
        
        $rows = $wpdb->get_results(
            "SELECT eponymous_id, ltv_total, aov, orders_count, recency_days,
                    top_brand_share, monetary_band, frequency_band, recency_band,
                    discount_share, price_band,
                    sole_order_share, avg_jlw, avg_vlw,
                    core_needs_json,
                    emails_sent_lifetime, open_rate_lifetime,
                    click_rate_lifetime, days_since_last_engagement,
                    taste_vector,
                    segments_json
             FROM {$tbl_ci}",
            ARRAY_A
        );
        if (empty($rows)) { return false; }

        $features        = array();
        $customer_needs  = array();
        $customer_taste  = array(); 
        foreach ($rows as $r) {
            $epo = (int) $r['eponymous_id'];
            $features[$epo] = $this->build_knn_feature_vector($r);
            $needs = !empty($r['core_needs_json']) ? json_decode((string) $r['core_needs_json'], true) : array();
            $customer_needs[$epo] = is_array($needs) ? $needs : array();
            
            
            $taste_raw = $this->unpack_taste_vector($r['taste_vector'] ?? null);
            $customer_taste[$epo] = $taste_raw === null ? null : $this->normalize_vector($taste_raw);
        }

        
        
        
        $grank_to_label = $this->get_f18_priority_map();
        if (empty($grank_to_label)) { return false; }
        $label_to_slug  = $this->build_label_to_slug_map($grank_to_label);
        $default_csv    = implode(',', array_keys($grank_to_label));

        $f18_rows = $wpdb->get_results(
            "SELECT eponymous_id, personal_priorities
             FROM {$tbl_si}
             WHERE f18_entry_id > 0
               AND personal_priorities IS NOT NULL AND personal_priorities <> ''",
            ARRAY_A
        );

        $labeled = array();
        foreach ($f18_rows as $r) {
            $epo = (int) $r['eponymous_id'];
            if (!isset($features[$epo])) { continue; }
            $cleaned = preg_replace('/\s+/', '', (string) $r['personal_priorities']);
            if ($cleaned === $default_csv) { continue; }

            $slugs = array();
            foreach (explode(',', (string) $r['personal_priorities']) as $g) {
                $g = trim($g);
                if (!isset($grank_to_label[$g])) { continue; }
                $label = $grank_to_label[$g];
                if (isset($label_to_slug[$label])) { $slugs[] = $label_to_slug[$label]; }
            }
            if (empty($slugs)) { continue; }
            $labeled[$epo] = array(
                'top1' => $slugs[0],
                'top3' => array_slice($slugs, 0, 3),
            );
        }

        if (count($labeled) < 50) {
            
            return false;
        }

        
        $labeled_epos    = array_keys($labeled);
        $labeled_vectors = array();
        $labeled_needs   = array();
        $labeled_taste   = array();
        foreach ($labeled_epos as $l_epo) {
            $labeled_vectors[] = $features[$l_epo];
            $labeled_needs[]   = isset($customer_needs[$l_epo]) ? $customer_needs[$l_epo] : array();
            $labeled_taste[]   = isset($customer_taste[$l_epo]) ? $customer_taste[$l_epo] : null;
        }
        $n_labeled = count($labeled_epos);
        $k         = min(20, $n_labeled);

        
        
        
        
        
        $NEEDS_WEIGHT = 3.0;
        
        
        
        
        
        $TASTE_WEIGHT = 4.0;

        
        
        
        $results = array();   

        
        
        
        
        $sub_total       = count($features);
        $sub_processed   = 0;
        $sub_started_at  = microtime(true);
        $sub_last_write  = $sub_started_at;
        $sub_status_seed = get_option(self::STATUS_OPTION, array());

        foreach ($features as $epo => $vec) {
            if (isset($labeled[$epo])) {
                $results[$epo] = array(
                    'slug'  => $labeled[$epo]['top1'],
                    'conf'  => 1.00,
                    'basis' => 'self_explicit',
                );
                continue;
            }

            
            
            
            
            
            
            $dim = count($vec);
            $self_needs = isset($customer_needs[$epo]) ? $customer_needs[$epo] : array();
            $self_taste = isset($customer_taste[$epo]) ? $customer_taste[$epo] : null;
            $distances = array();
            foreach ($labeled_vectors as $idx => $lvec) {
                $d = 0.0;
                for ($i = 0; $i < $dim; $i++) {
                    $delta = $vec[$i] - $lvec[$i];
                    $d += $delta * $delta;
                }
                $other_needs = $labeled_needs[$idx];
                if (!empty($self_needs) && !empty($other_needs)) {
                    $cos = $this->needs_cosine_similarity($self_needs, $other_needs);
                    $needs_dist = 1.0 - $cos; 
                    $d += pow($needs_dist * $NEEDS_WEIGHT, 2);
                }
                $other_taste = $labeled_taste[$idx];
                if ($self_taste !== null && $other_taste !== null) {
                    $tcos = $this->dot_product($self_taste, $other_taste);
                    $taste_dist = 1.0 - $tcos;
                    $d += pow($taste_dist * $TASTE_WEIGHT, 2);
                }
                $distances[$idx] = $d;
            }
            asort($distances);
            $top_idx = array_slice($distances, 0, $k, true);

            
            $vote = array();
            foreach ($top_idx as $idx => $sq_dist) {
                $weight = 1.0 / (1.0 + sqrt($sq_dist));
                $l_epo = $labeled_epos[$idx];
                foreach ($labeled[$l_epo]['top3'] as $rank_idx => $slug) {
                    $rank_w = (3 - $rank_idx) / 3.0; 
                    if (!isset($vote[$slug])) { $vote[$slug] = 0.0; }
                    $vote[$slug] += $weight * $rank_w;
                }
            }
            if (empty($vote)) {
                $results[$epo] = array('slug' => 'general', 'conf' => 0.00, 'basis' => 'no_vote');
                $sub_processed++;
            } else {
                arsort($vote);
                $total = array_sum($vote);
                $top_slug = key($vote);
                $top_score = current($vote);
                $conf = $total > 0 ? round($top_score / $total, 2) : 0;
                $results[$epo] = array('slug' => $top_slug, 'conf' => $conf, 'basis' => 'knn_' . $k);
                $sub_processed++;
            }

            
            
            
            
            
            $now = microtime(true);
            if (($now - $sub_last_write) >= 30) {
                $cur = get_option(self::STATUS_OPTION, $sub_status_seed);
                if (is_array($cur)) {
                    $cur['subprogress'] = array(
                        'processed' => $sub_processed,
                        'total'     => $sub_total,
                        'label'     => __('Customers scanned (k-NN)', 'ai-eshop-optimizer'),
                        'elapsed'   => (int) round($now - $sub_started_at),
                    );
                    update_option(self::STATUS_OPTION, $cur, false);
                }
                $sub_last_write = $now;
            }
        }

        
        
        $cur = get_option(self::STATUS_OPTION, $sub_status_seed);
        if (is_array($cur)) {
            $cur['subprogress'] = array(
                'processed' => $sub_processed,
                'total'     => $sub_total,
                'label'     => __('Customers scanned (k-NN)', 'ai-eshop-optimizer'),
                'elapsed'   => (int) round(microtime(true) - $sub_started_at),
            );
            update_option(self::STATUS_OPTION, $cur, false);
        }

        
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_knn_results");
        $wpdb->query(
            "CREATE TEMPORARY TABLE _aieo_knn_results (
                eponymous_id BIGINT UNSIGNED PRIMARY KEY,
                slug VARCHAR(40) NOT NULL,
                conf DECIMAL(3,2) NOT NULL DEFAULT 0,
                basis VARCHAR(40) NOT NULL DEFAULT ''
             ) ENGINE=MEMORY"
        );

        $chunks = array_chunk($results, 1000, true);
        foreach ($chunks as $chunk) {
            $values = array();
            $args   = array();
            foreach ($chunk as $epo => $r) {
                $values[] = '(%d, %s, %f, %s)';
                $args[] = (int) $epo;
                $args[] = (string) $r['slug'];
                $args[] = (float) $r['conf'];
                $args[] = (string) $r['basis'];
            }
            $sql = "INSERT INTO _aieo_knn_results (eponymous_id, slug, conf, basis) VALUES "
                 . implode(',', $values);
            $wpdb->query($wpdb->prepare($sql, ...$args));
        }

        $wpdb->query(
            "UPDATE {$tbl_ci} ci
             JOIN _aieo_knn_results r ON r.eponymous_id = ci.eponymous_id
             SET ci.inferred_top_priority          = r.slug,
                 ci.inferred_priorities_confidence = r.conf,
                 ci.inferred_priorities_json       = CONCAT('{\"basis\":\"', r.basis, '\"}'),
                 ci.inferred_priorities_source     = 'knn',
                 ci.inferred_priorities_updated_at = UTC_TIMESTAMP()"
        );
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_knn_results");

        
        
        delete_transient('aieo_surveys_knn_loo_eval');

        return array(
            'labeled'        => count($labeled),
            'total'          => count($features),
            'k'              => $k,
        );
    }

    
    private function needs_cosine_similarity(array $a, array $b) {
        if (empty($a) || empty($b)) { return 0.0; }
        $dot = 0.0; $mag_a_sq = 0.0; $mag_b_sq = 0.0;
        foreach ($a as $k => $v) {
            $v = (float) $v;
            $mag_a_sq += $v * $v;
            if (isset($b[$k])) {
                $dot += $v * (float) $b[$k];
            }
        }
        foreach ($b as $v) {
            $v = (float) $v;
            $mag_b_sq += $v * $v;
        }
        if ($mag_a_sq <= 0 || $mag_b_sq <= 0) { return 0.0; }
        return $dot / sqrt($mag_a_sq * $mag_b_sq);
    }

    
    private function normalize_vector(array $v) {
        $sum = 0.0;
        foreach ($v as $x) { $sum += $x * $x; }
        if ($sum <= 0) { return $v; }
        $inv = 1.0 / sqrt($sum);
        $out = array();
        foreach ($v as $x) { $out[] = $x * $inv; }
        return $out;
    }

    
    private function dot_product(array $a, array $b) {
        $n = min(count($a), count($b));
        $d = 0.0;
        for ($i = 0; $i < $n; $i++) {
            $d += (float) $a[$i] * (float) $b[$i];
        }
        return $d;
    }

    
    private function dense_cosine_similarity(array $a, array $b) {
        $n = min(count($a), count($b));
        if ($n === 0) { return 0.0; }
        $dot = 0.0; $ma = 0.0; $mb = 0.0;
        for ($i = 0; $i < $n; $i++) {
            $av = (float) $a[$i];
            $bv = (float) $b[$i];
            $dot += $av * $bv;
            $ma  += $av * $av;
            $mb  += $bv * $bv;
        }
        if ($ma <= 0 || $mb <= 0) { return 0.0; }
        return $dot / sqrt($ma * $mb);
    }

    
    private function unpack_taste_vector($blob) {
        if (empty($blob)) { return null; }
        $bytes = strlen($blob);
        if ($bytes < 4 || ($bytes % 4) !== 0) { return null; }
        $arr = unpack('g*', $blob);
        if ($arr === false) { return null; }
        return array_values($arr);
    }

    
    private function pack_taste_vector(array $vec) {
        if (empty($vec)) { return null; }
        return pack('g*', ...array_map('floatval', $vec));
    }

    
    public function compute_taste_vectors_batch($batch_size = 50) {
        global $wpdb;
        $tbl_ci  = $wpdb->prefix . 'aieo_customer_intelligence';
        $tbl_emb = $wpdb->prefix . 'aieo_chat_embeddings';
        @set_time_limit(0);

        
        if ((string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl_emb)) !== $tbl_emb) {
            return array('skipped' => 'embeddings_table_missing');
        }

        
        
        
        
        $limit_clause = $batch_size > 0 ? 'LIMIT ' . (int) $batch_size : '';
        $candidates = $wpdb->get_results(
            "SELECT eponymous_id,
                    top_sole_product_id, top_first_product_id, top_last_product_id
             FROM {$tbl_ci}
             WHERE (top_sole_product_id > 0 OR top_first_product_id > 0 OR top_last_product_id > 0)
               AND (taste_vector IS NULL
                    OR taste_vector_updated_at IS NULL
                    OR (last_order_at IS NOT NULL AND last_order_at > taste_vector_updated_at))
             ORDER BY last_order_at DESC
             {$limit_clause}",
            ARRAY_A
        );

        $stats = array(
            'candidates' => count($candidates),
            'vectorised' => 0,
            'skipped_no_embeddings' => 0,
        );
        if (empty($candidates)) { return $stats; }

        
        
        
        $engine = get_option('aieo_chat_embedding_model', 'voyage-2');

        
        
        $all_pids = array();
        foreach ($candidates as $c) {
            foreach (array('top_sole_product_id', 'top_first_product_id', 'top_last_product_id') as $col) {
                $pid = (int) $c[$col];
                if ($pid > 0) { $all_pids[$pid] = true; }
            }
        }
        $all_pids = array_keys($all_pids);

        $embeddings_by_pid = array();
        $chunk_size = 500;
        foreach (array_chunk($all_pids, $chunk_size) as $chunk) {
            $placeholders = implode(',', array_fill(0, count($chunk), '%d'));
            $sql = "SELECT post_id, embedding
                    FROM {$tbl_emb}
                    WHERE post_id IN ({$placeholders})
                      AND engine_type = %s
                      AND embedding IS NOT NULL
                      AND LENGTH(embedding) > 100";
            $args = array_merge($chunk, array($engine));
            $rows = $wpdb->get_results($wpdb->prepare($sql, ...$args), ARRAY_A);
            foreach ($rows as $r) {
                $vec = $this->decode_chat_embedding((string) $r['embedding']);
                if (is_array($vec) && !empty($vec)) {
                    $embeddings_by_pid[(int) $r['post_id']] = $vec;
                }
            }
        }

        
        $rows_to_write = array();
        foreach ($candidates as $c) {
            $vecs = array();
            foreach (array('top_sole_product_id', 'top_first_product_id', 'top_last_product_id') as $col) {
                $pid = (int) $c[$col];
                if ($pid > 0 && isset($embeddings_by_pid[$pid])) {
                    $vecs[] = $embeddings_by_pid[$pid];
                }
            }
            if (empty($vecs)) {
                $stats['skipped_no_embeddings']++;
                continue;
            }
            $avg = $this->average_dense_vectors($vecs);
            $blob = $this->pack_taste_vector($avg);
            if ($blob === null) { continue; }
            $rows_to_write[(int) $c['eponymous_id']] = $blob;
            $stats['vectorised']++;
        }

        if (!empty($rows_to_write)) {
            
            
            $now = current_time('mysql', true);
            foreach ($rows_to_write as $epo => $blob) {
                $wpdb->query($wpdb->prepare(
                    "UPDATE {$tbl_ci}
                     SET taste_vector = %s, taste_vector_updated_at = %s
                     WHERE eponymous_id = %d",
                    $blob, $now, $epo
                ));
            }
        }

        
        
        
        
        
        

        return $stats;
    }

    
    private function decode_chat_embedding($raw) {
        if (empty($raw)) { return null; }
        $decoded = base64_decode($raw, true);
        if ($decoded === false) { return null; }
        $decompressed = @gzuncompress($decoded);
        if ($decompressed === false || $decompressed === '') { return null; }
        $unpacked = @unpack('f*', $decompressed);
        if (!is_array($unpacked)) { return null; }
        return array_values($unpacked);
    }

    
    private function average_dense_vectors(array $vecs) {
        $count = count($vecs);
        if ($count === 0) { return array(); }
        if ($count === 1) { return $vecs[0]; }
        $dim = count($vecs[0]);
        $sum = array_fill(0, $dim, 0.0);
        foreach ($vecs as $v) {
            $vd = min($dim, count($v));
            for ($i = 0; $i < $vd; $i++) {
                $sum[$i] += (float) $v[$i];
            }
        }
        for ($i = 0; $i < $dim; $i++) {
            $sum[$i] /= $count;
        }
        return $sum;
    }

    
    private function build_knn_feature_vector(array $r) {
        $segs = !empty($r['segments_json']) ? json_decode((string) $r['segments_json'], true) : array();
        $segs = is_array($segs) ? $segs : array();
        $segs_set = array_flip($segs); 

        
        
        
        
        
        $discount_share = (float) ($r['discount_share'] ?? 0);

        
        
        
        $emails_sent = (int) ($r['emails_sent_lifetime'] ?? 0);
        $open_rate   = (float) ($r['open_rate_lifetime'] ?? 0);
        $click_rate  = (float) ($r['click_rate_lifetime'] ?? 0);
        $days_since  = (int) ($r['days_since_last_engagement'] ?? 65535);
        $engagement_recency = 1.0 - min(1.0, $days_since / 365.0);
        $sent_breadth       = log10($emails_sent + 1) / 3.0; 

        return array(
            (float) log10(((float) $r['ltv_total'])     + 1),
            (float) log10(((float) $r['aov'])           + 1),
            (float) log10(((int)   $r['orders_count']) + 1),
            min(3.0, ((int) $r['recency_days']) / 365.0),
            (float) $r['top_brand_share'],
            ((int) $r['monetary_band'])  / 5.0,
            ((int) $r['frequency_band']) / 5.0,
            ((int) $r['recency_band'])   / 5.0,
            isset($segs_set['vip'])             ? 1.0 : 0.0,
            isset($segs_set['loyalist'])        ? 1.0 : 0.0,
            isset($segs_set['newbie'])          ? 1.0 : 0.0,
            isset($segs_set['replenisher'])     ? 1.0 : 0.0,
            isset($segs_set['brand_devoted'])   ? 1.0 : 0.0,
            isset($segs_set['dormant'])         ? 1.0 : 0.0,
            isset($segs_set['lapsed'])          ? 1.0 : 0.0,
            isset($segs_set['discount_hunter']) ? 1.0 : 0.0,
            $discount_share * 2.0,                               
            ((int) ($r['price_band'] ?? 0))  / 5.0,              
            
            
            
            ((float) ($r['sole_order_share'] ?? 0)) * 2.0,       
            ((float) ($r['avg_jlw']          ?? 0)),             
            ((float) ($r['avg_vlw']          ?? 0)),             
            
            
            
            $sent_breadth,                                        
            $open_rate,                                           
            $click_rate * 1.5,                                    
            $engagement_recency,                                  
        );
    }

    
    public function backfill_eponymous_via_order_id() {
        global $wpdb;
        $tables = $this->discover_order_snapshot_tables();
        if (empty($tables)) { return false; }

        $tbl_sr = $wpdb->prefix . 'aieo_survey_responses';
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';

        
        
        
        
        
        $parts = array();
        foreach ($tables as $t) {
            $parts[] = "SELECT DISTINCT OrderId, EponymousID FROM `{$t}` WHERE OrderId > 0 AND EponymousID IS NOT NULL AND EponymousID > 0";
        }
        $union = implode(' UNION ALL ', $parts);

        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_order_to_epo");
        $wpdb->query(
            "CREATE TEMPORARY TABLE _aieo_order_to_epo (
                OrderId BIGINT UNSIGNED NOT NULL PRIMARY KEY,
                EponymousID BIGINT UNSIGNED NOT NULL
             ) ENGINE=MEMORY"
        );
        
        
        $wpdb->query(
            "INSERT IGNORE INTO _aieo_order_to_epo (OrderId, EponymousID)
             SELECT OrderId, EponymousID FROM ({$union}) raw"
        );

        
        $wpdb->query(
            "UPDATE {$tbl_sr} sr
             JOIN _aieo_order_to_epo o ON o.OrderId = sr.order_id
             SET sr.eponymous_id = o.EponymousID
             WHERE sr.order_id > 0 AND sr.eponymous_id = 0"
        );

        
        $wpdb->query(
            "UPDATE {$tbl_sr} sr
             JOIN {$tbl_ci} ci ON ci.eponymous_id = sr.eponymous_id
             SET sr.wp_user_id = ci.wp_user_id
             WHERE sr.eponymous_id > 0 AND sr.wp_user_id = 0 AND ci.wp_user_id > 0"
        );

        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_order_to_epo");
        return true;
    }

    public function compute_purchase_metrics() {
        global $wpdb;
        $tables = $this->discover_order_snapshot_tables();
        if (empty($tables)) { return false; }

        $tbl_si = $wpdb->prefix . 'aieo_survey_intelligence';

        $parts = array();
        foreach ($tables as $t) {
            $parts[] = "SELECT EponymousID, OrderDate, OrderId, LineValue FROM `{$t}` WHERE EponymousID > 0";
        }
        $union = implode(' UNION ALL ', $parts);

        
        
        
        
        
        
        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_surv_orders");
        $wpdb->query(
            "CREATE TEMPORARY TABLE _aieo_surv_orders (
                EponymousID BIGINT UNSIGNED NOT NULL,
                OrderDate DATETIME NOT NULL,
                OrderTotal DECIMAL(12,2) NOT NULL,
                KEY idx_epo_date (EponymousID, OrderDate)
             ) ENGINE=MEMORY"
        );
        $wpdb->query(
            "INSERT INTO _aieo_surv_orders (EponymousID, OrderDate, OrderTotal)
             SELECT EponymousID, MIN(OrderDate), SUM(IFNULL(LineValue,0))
             FROM ({$union}) raw
             GROUP BY EponymousID, OrderId"
        );

        
        
        
        $wpdb->query(
            "UPDATE {$tbl_si} si
             LEFT JOIN (
                 SELECT
                     ao.EponymousID,
                     SUM(CASE WHEN ao.OrderDate <  si2.f18_submitted_at THEN 1 ELSE 0 END) AS ob,
                     SUM(CASE WHEN ao.OrderDate <  si2.f18_submitted_at THEN ao.OrderTotal ELSE 0 END) AS sb,
                     SUM(CASE WHEN ao.OrderDate >= si2.f18_submitted_at THEN 1 ELSE 0 END) AS oa,
                     SUM(CASE WHEN ao.OrderDate >= si2.f18_submitted_at THEN ao.OrderTotal ELSE 0 END) AS sa
                 FROM _aieo_surv_orders ao
                 JOIN {$tbl_si} si2 ON si2.eponymous_id = ao.EponymousID
                 WHERE si2.f18_submitted_at IS NOT NULL
                 GROUP BY ao.EponymousID
             ) agg ON agg.EponymousID = si.eponymous_id
             SET si.orders_before_survey = IFNULL(agg.ob, 0),
                 si.spend_before_survey  = IFNULL(agg.sb, 0),
                 si.orders_after_survey  = IFNULL(agg.oa, 0),
                 si.spend_after_survey   = IFNULL(agg.sa, 0)"
        );

        $wpdb->query("DROP TEMPORARY TABLE IF EXISTS _aieo_surv_orders");
        return true;
    }

    
    
    

    
    public function ajax_recompute() {
        AIEO_DMM::verify_ajax_request();
        
        
        
        
        
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array(
                'message' => __('Recompute requires administrator privileges.', 'ai-eshop-optimizer'),
            ), 403);
        }
        $resp = $this->kick_off_async('recompute');
        if ($resp === 'busy') {
            wp_send_json_error(array('message' => __('Another job is already running — wait for it to finish.', 'ai-eshop-optimizer')));
        }
        wp_send_json_success(array(
            'message' => __('Recompute queued — running in background…', 'ai-eshop-optimizer'),
            'kind'    => 'recompute',
            'queued'  => true,
        ));
    }

    public function ajax_ingest() {
        AIEO_DMM::verify_ajax_request();
        $resp = $this->kick_off_async('ingest');
        if ($resp === 'busy') {
            wp_send_json_error(array('message' => __('Another job is already running — wait for it to finish.', 'ai-eshop-optimizer')));
        }
        wp_send_json_success(array(
            'message' => __('Ingest queued — running in background…', 'ai-eshop-optimizer'),
            'kind'    => 'ingest',
            'queued'  => true,
        ));
    }

    
    public function ajax_status() {
        AIEO_DMM::verify_ajax_request();
        $status = get_option(self::STATUS_OPTION, array(
            'kind' => '', 'state' => 'idle', 'started' => 0, 'finished' => 0, 'message' => '',
        ));
        wp_send_json_success($status);
    }

    public function ajax_bootstrap_ci() {
        AIEO_DMM::verify_ajax_request();
        $created = $this->ensure_customer_intelligence_table();
        if ($created) {
            wp_send_json_success(array('message' => __('Table created. Now open the Web Push tab and run "Recompute customer intelligence" to populate it.', 'ai-eshop-optimizer')));
        }
        wp_send_json_error(array('message' => __('Could not create wp_aieo_customer_intelligence — check debug.log.', 'ai-eshop-optimizer')));
    }

    public function ajax_verify_prices() {
        AIEO_DMM::verify_ajax_request();
        $result = $this->verify_price_historical_status();
        if (!$result['ok']) {
            wp_send_json_error(array('message' => $result['error'] ?? 'Unknown error.'));
        }
        wp_send_json_success(array('rows' => $result['rows']));
    }

    public function ajax_fix_prices() {
        AIEO_DMM::verify_ajax_request();
        @set_time_limit(0);
        @ignore_user_abort(true);
        $start = microtime(true); 
        try {
            $result = $this->compute_discount_and_price_from_wc();
        } catch (\Throwable $e) {
            
            wp_send_json_error(array('message' => sprintf( __('Failed: %s', 'ai-eshop-optimizer'), $e->getMessage() )));
        }
        if ($result === false) {
            wp_send_json_error(array('message' => __('No order snapshot tables found OR wp_woocommerce_order_items missing. Check Web Push CI is populated and WC tables exist.', 'ai-eshop-optimizer')));
        }
        $duration = round(microtime(true) - $start, 2);
        wp_send_json_success(array(
            'message' => sprintf(
                
                __('Computed from WC line items in %4$ss — %1$d customers with discount data, %2$d with price_band assigned, avg discount_share = %3$s.', 'ai-eshop-optimizer'),
                (int) $result['with_discount_data'],
                (int) $result['with_price_band'],
                number_format_i18n((float) $result['avg_discount_share'], 4),
                $duration
            ),
            'stats' => $result,
        ));
    }

    
    private function kick_off_async($kind) {
        $this->maybe_install();

        $current = get_option(self::STATUS_OPTION, array());
        $cur_state   = is_array($current) ? (string) ($current['state']   ?? 'idle') : 'idle';
        $cur_started = is_array($current) ? (int)    ($current['started'] ?? 0)      : 0;
        $stale_after = 20 * MINUTE_IN_SECONDS;
        if (($cur_state === 'queued' || $cur_state === 'running') && (time() - $cur_started) < $stale_after) {
            return 'busy';
        }

        update_option(self::STATUS_OPTION, array(
            'kind'     => $kind,
            'state'    => 'queued',
            'started'  => time(),
            'finished' => 0,
            'message'  => '',
        ), false);
        if (!wp_next_scheduled(self::CRON_ASYNC_HOOK, array($kind))) {
            wp_schedule_single_event(time() - 1, self::CRON_ASYNC_HOOK, array($kind));
        }
        if (function_exists('spawn_cron')) {
            spawn_cron();
        }
        return 'queued';
    }

    
    public function on_async_run($kind) {
        global $wpdb;
        @set_time_limit(0);
        @ignore_user_abort(true);
        $this->maybe_install(); 

        $start = microtime(true);
        $status = array(
            'kind'     => $kind,
            'state'    => 'running',
            'started'  => time(),
            'finished' => 0,
            'message'  => '',
            'stats'    => array(),
        );
        update_option(self::STATUS_OPTION, $status, false);

        try {
            if ($kind === 'ingest') {
                $tbl_sr = $wpdb->prefix . 'aieo_survey_responses';
                $rows_before = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_sr}");

                
                $t_a = microtime(true);
                $wpdb->query('CALL aieo_sp_ingest_survey_responses()');
                $dur_a = round(microtime(true) - $t_a, 2);

                
                $t_b = microtime(true);
                try {
                    $this->backfill_eponymous_via_order_id();
                } catch (\Throwable $e) {
                    if (function_exists('aieo_debug_log')) {
                        aieo_debug_log('AIEO_Surveys backfill_eponymous_via_order_id failed: ' . $e->getMessage());
                    }
                }
                $dur_b = round(microtime(true) - $t_b, 2);

                $rows_after  = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_sr}");
                $unresolved  = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_sr} WHERE eponymous_id = 0");
                $duration    = round(microtime(true) - $start, 2);
                $status['message'] = sprintf(
                    
                    __('Ingested %1$d responses (+%2$d new, %3$d unresolved) in %4$ss — pivot %5$ss, backfill %6$ss.', 'ai-eshop-optimizer'),
                    $rows_after, max(0, $rows_after - $rows_before), $unresolved, $duration, $dur_a, $dur_b
                );
                $status['stats'] = compact('rows_before', 'rows_after', 'unresolved', 'duration', 'dur_a', 'dur_b');
            } elseif ($kind === 'recompute') {
                $tbl_si = $wpdb->prefix . 'aieo_survey_intelligence';
                $rows_before = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_si}");
                $wpdb->query('CALL aieo_sp_extract_survey_intelligence()');

                
                
                
                
                
                $stages = array(
                    array('key' => 'purchase_metrics',    'label' => __('Purchase metrics (LTV / AOV / RFM)', 'ai-eshop-optimizer'),
                          'fn'  => function () { $this->compute_purchase_metrics(); }),
                    array('key' => 'discount_price',      'label' => __('Discount share + price band', 'ai-eshop-optimizer'),
                          'fn'  => function () { $this->compute_discount_and_price_from_wc(); }),
                    array('key' => 'segments',            'label' => __('Segment assignment', 'ai-eshop-optimizer'),
                          'fn'  => function () use ($wpdb) { $wpdb->query('CALL aieo_sp_assign_segments()'); }),
                    array('key' => 'position_signals',    'label' => __('Position signals (sole / first / last)', 'ai-eshop-optimizer'),
                          'fn'  => function () { $this->compute_position_signals_from_temp_orders(); }),
                    array('key' => 'core_needs',          'label' => __('Core needs (pa_need aggregation)', 'ai-eshop-optimizer'),
                          'fn'  => function () { $this->compute_core_needs_from_top_products(); }),
                    array('key' => 'tnp_engagement',      'label' => __('TNP newsletter engagement', 'ai-eshop-optimizer'),
                          'fn'  => function () { $this->compute_tnp_engagement_signals(); }),
                    array('key' => 'taste_vectors',       'label' => __('Voyage taste vectors (batch 500)', 'ai-eshop-optimizer'),
                          'fn'  => function () { $this->compute_taste_vectors_batch(500); }),
                    array('key' => 'priority_inference',  'label' => __('Priority inference (k-NN, heuristic fallback)', 'ai-eshop-optimizer'),
                          'fn'  => function () {
                              $r = $this->compute_inferred_priorities_knn();
                              if ($r === false) { $this->compute_inferred_priorities_heuristic(); }
                          }),
                );

                $total = count($stages);
                $stage_stats = array();
                foreach ($stages as $idx => $stage) {
                    $current_started = microtime(true);
                    
                    
                    
                    $status['progress'] = array(
                        'current'   => $idx + 1,
                        'total'     => $total,
                        'key'       => $stage['key'],
                        'label'     => (string) $stage['label'],
                        'started'   => time(),
                    );
                    
                    
                    
                    
                    
                    unset($status['subprogress']);
                    update_option(self::STATUS_OPTION, $status, false);

                    try {
                        call_user_func($stage['fn']);
                        $stage_stats[$stage['key']] = round(microtime(true) - $current_started, 2);
                    } catch (\Throwable $e) {
                        $stage_stats[$stage['key']] = 'error: ' . $e->getMessage();
                        if (function_exists('aieo_debug_log')) {
                            aieo_debug_log('AIEO_Surveys recompute stage "' . $stage['key'] . '" failed: ' . $e->getMessage());
                        }
                    }
                }

                $rows_after = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_si}");
                $duration   = round(microtime(true) - $start, 2);
                $status['message'] = sprintf(
                    
                    __('Recomputed: %1$d profiles (+%2$d new) in %3$ss.', 'ai-eshop-optimizer'),
                    $rows_after, max(0, $rows_after - $rows_before), $duration
                );
                $status['stats'] = compact('rows_before', 'rows_after', 'duration', 'stage_stats');
                unset($status['progress']); 
            } else {
                throw new \RuntimeException('Unknown async kind: ' . $kind);
            }
            $status['state'] = 'done';
        } catch (\Throwable $e) {
            $status['state']   = 'error';
            $status['message'] = $e->getMessage();
            if (function_exists('aieo_debug_log')) {
                aieo_debug_log('AIEO_Surveys async ' . $kind . ' failed: ' . $e->getMessage());
            }
        }
        $status['finished'] = time();
        update_option(self::STATUS_OPTION, $status, false);
    }

    
    
    

    public function render_subtab() {
        global $wpdb;
        $tbl_si = $wpdb->prefix . 'aieo_survey_intelligence';
        $tbl_sr = $wpdb->prefix . 'aieo_survey_responses';
        $exists = (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl_si)) === $tbl_si
               && (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl_sr)) === $tbl_sr;

        echo '<div class="aieo-dmm-section">';
        echo '<h2>' . esc_html__('Customer Intelligence', 'ai-eshop-optimizer') . '</h2>';
        echo '<p class="description">'
            . esc_html__('The full per-customer CDP. Combines order signals (LTV, AOV, RFM, segments, position-based first/last/sole products), newsletter engagement (TNP open/click rates), Voyage taste vectors over signature products, and — when filled — Gravity Forms #18/#20 voice-of-customer data. Built from wp_aieo_temp_orders + the catalog flat table; voice-of-customer rows mirrored live on form submit and re-synced nightly at 03:00 Athens. Drives audience selection, push triggers, newsletter cohorts, and inferred priorities.', 'ai-eshop-optimizer')
            . '</p>';

        if (!$exists) {
            echo '<div class="aieo-dmm-card"><p>'
                . esc_html__('Survey tables are being installed. Reload once to finish setup.', 'ai-eshop-optimizer')
                . '</p></div></div>';
            return;
        }

        
        
        
        
        
        
        echo '<div class="aieo-dmm-section-banner" style="margin:18px 0 10px;padding:12px 14px;background:#eff6ff;border-left:3px solid #2563eb;border-radius:4px;">';
        echo '<h2 style="margin:0 0 4px;font-size:16px;">' . esc_html__('Initial setup — run once, in order', 'ai-eshop-optimizer') . '</h2>';
        echo '<p style="margin:0;font-size:12px;color:#475569;">'
            . esc_html__('Steps ① → ② → ③ → ④ each depend on the previous one. After this completes once, the nightly cron at 02:30–03:00 Athens refreshes everything automatically — the buttons below are then just manual on-demand refreshes.', 'ai-eshop-optimizer')
            . '</p>';
        echo '</div>';

        $this->render_dependency_status_card();      
        $this->render_pipeline_card($tbl_sr, $tbl_si); 
        $this->render_recompute_card($tbl_si);        
        $this->render_inferred_priorities_card();     

        
        echo '<div class="aieo-dmm-section-banner" style="margin:28px 0 10px;padding:12px 14px;background:#f5f3ff;border-left:3px solid #7c3aed;border-radius:4px;">';
        echo '<h2 style="margin:0 0 4px;font-size:16px;">' . esc_html__('Analytics & diagnostics', 'ai-eshop-optimizer') . '</h2>';
        echo '<p style="margin:0;font-size:12px;color:#475569;">'
            . esc_html__('Read-only views over the data the four setup steps produce. Refresh on page reload — no buttons to press here.', 'ai-eshop-optimizer')
            . '</p>';
        echo '</div>';

        $this->render_voc_distribution_card($tbl_si);
        $this->render_nps_distribution_card($tbl_si);
        $this->render_priority_distribution_card($tbl_si);
        $this->render_priority_diagnostics_card();
        $this->render_position_diagnostics_card();
        $this->render_engagement_diagnostics_card();
        $this->render_push_engagement_diagnostics_card();
        $this->render_taste_vectors_card();
        $this->render_loyalty_journey_card($tbl_si);
        $this->render_repeat_respondents_card($tbl_sr);
        $this->render_metric_distributions_card($tbl_si);
        $this->render_actionable_lists_card($tbl_si);
        $this->render_competition_signal_card();
        $this->render_phase5_todo_card();

        echo '</div>';
    }

    
    private function render_dependency_status_card() {
        global $wpdb;
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';
        $tbl_sr = $wpdb->prefix . 'aieo_survey_responses';
        $tbl_si = $wpdb->prefix . 'aieo_survey_intelligence';

        $exists = function ($t) use ($wpdb) {
            return (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $t)) === $t;
        };

        $ci_ok = $exists($tbl_ci);
        $sr_ok = $exists($tbl_sr);
        $si_ok = $exists($tbl_si);

        $ci_rows = $ci_ok ? (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_ci}") : 0;
        $sr_rows = $sr_ok ? (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_sr}") : 0;
        $si_rows = $si_ok ? (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_si}") : 0;

        $all_ok = $ci_ok && $sr_ok && $si_ok && $ci_rows > 0;
        $bg     = $all_ok ? '#ecfdf5' : '#fef2f2';
        $border = $all_ok ? '#15803d' : '#b32d2e';

        echo '<div class="aieo-dmm-card" style="background:' . esc_attr($bg) . ';border-left:3px solid ' . esc_attr($border) . ';">';
        echo '<h3>' . esc_html__('① Dependencies — Customer Intelligence base', 'ai-eshop-optimizer') . '</h3>';
        echo '<div class="aieo-attr-tiles">';
        $this->tile('wp_aieo_customer_intelligence', $ci_ok ? number_format_i18n($ci_rows) . ' rows' : '— missing —',
            __('The master CDP table — one row per known customer (eponymous_id). Built nightly from order snapshots, sessions, wishlist, ratings + this survey data. Powers segmentation, push triggers, AOV/LTV.', 'ai-eshop-optimizer'));
        $this->tile('wp_aieo_survey_responses',     $sr_ok ? number_format_i18n($sr_rows) . ' rows' : '— missing —',
            __('Mirror of every Gravity Forms F18/F20 entry, one row per submission. Created live on form submit and re-synced nightly at 03:00 Athens via the ingestion pipeline.', 'ai-eshop-optimizer'));
        $this->tile('wp_aieo_survey_intelligence',  $si_ok ? number_format_i18n($si_rows) . ' rows' : '— missing —',
            __('Per-customer survey rollup, one row per eponymous_id who responded at least once. Joins F18 + F20 answers, NPS, satisfaction, priorities, plus orders_before/after_survey and spend_before/after_survey.', 'ai-eshop-optimizer'));
        echo '</div>';

        if (!$ci_ok) {
            echo '<p style="margin:10px 0 0;font-size:13px;color:#7f1d1d;">'
                . esc_html__('customer_intelligence is the Web Push module table — Phase 1 of the AIEO CDP. Without it, eponymous resolution, purchase correlation and inferred priorities all fail. The Surveys module tries to auto-create the schema on every page load, but the table also needs to be populated by the Web Push tab\'s "Recompute customer intelligence" job (which scans your wp_aieo_*_orders snapshots).', 'ai-eshop-optimizer')
                . '</p>';
            echo '<p style="margin:6px 0 0;font-size:13px;">';
            echo '<button type="button" class="button" id="aieo-surveys-bootstrap-ci">'
                . esc_html__('Force-create the table now', 'ai-eshop-optimizer')
                . '</button>';
            echo ' <span id="aieo-surveys-bootstrap-ci-status" style="margin-left:10px;color:#646970;font-size:13px;"></span>';
            echo '</p>';
            echo $this->bootstrap_ci_inline_js(); 
        } elseif ($ci_rows === 0) {
            echo '<p style="margin:10px 0 0;font-size:13px;color:#78350f;">' 
                . esc_html__('customer_intelligence table exists but has 0 rows. The Web Push module needs to scan your wp_aieo_*_orders snapshots to populate the ~33k per-customer rows. Click below to trigger that recompute directly from here, or navigate to the Web Push tab to do it there.', 'ai-eshop-optimizer')
                . '</p>';
            echo '<p style="margin:6px 0 0;font-size:13px;">';
            echo '<button type="button" class="button button-primary" id="aieo-surveys-populate-ci">'
                . esc_html__('Populate Customer Intelligence (one-time — scans your order snapshots)', 'ai-eshop-optimizer')
                . '</button>';
            echo ' <span id="aieo-surveys-populate-ci-status" style="margin-left:10px;color:#646970;font-size:13px;"></span>';
            echo '</p>';
            echo $this->populate_ci_inline_js(); 
        }
        echo '</div>'; 
    }

    
    private function populate_ci_inline_js() {
        $nonce = wp_create_nonce(AIEO_DMM::NONCE_ACTION);
        $ajax  = admin_url('admin-ajax.php');
        ob_start();
        ?>
        <script>
        (function(){
            const btn = document.getElementById('aieo-surveys-populate-ci');
            const status = document.getElementById('aieo-surveys-populate-ci-status');
            if (!btn) return;
            btn.addEventListener('click', async function(){
                btn.disabled = true;
                status.style.color = '#646970';
                status.textContent = 'Recomputing — scanning order snapshots, this may take 30-60s…';
                try {
                    const fd = new FormData();
                    fd.set('action', 'aieo_dmm_webpush_recompute');
                    fd.set('nonce', '<?php echo esc_js($nonce); ?>');
                    const resp = await fetch('<?php echo esc_js($ajax); ?>', { method:'POST', body:fd, credentials:'same-origin' });
                    const text = await resp.text();
                    let data;
                    try { data = JSON.parse(text); }
                    catch(_) {
                        status.style.color = '#b32d2e';
                        status.textContent = 'Server response not JSON: ' + text.slice(0, 200);
                        btn.disabled = false;
                        return;
                    }
                    if (data && data.success) {
                        status.style.color = '#15803d';
                        status.textContent = (data.data && data.data.message) || 'Done.';
                        setTimeout(() => window.location.reload(), 1500);
                    } else {
                        status.style.color = '#b32d2e';
                        status.textContent = (data && data.data && data.data.message) || 'Failed.';
                        btn.disabled = false;
                    }
                } catch (e) {
                    status.style.color = '#b32d2e';
                    status.textContent = String(e);
                    btn.disabled = false;
                }
            });
        })();
        </script>
        <?php
        return ob_get_clean();
    }

    private function bootstrap_ci_inline_js() {
        $nonce = wp_create_nonce(AIEO_DMM::NONCE_ACTION);
        $ajax  = admin_url('admin-ajax.php');
        ob_start();
        ?>
        <script>
        (function(){
            const btn = document.getElementById('aieo-surveys-bootstrap-ci');
            const status = document.getElementById('aieo-surveys-bootstrap-ci-status');
            if (!btn) return;
            btn.addEventListener('click', async function(){
                btn.disabled = true;
                status.textContent = 'Creating…';
                const fd = new FormData();
                fd.set('action', 'aieo_dmm_surveys_bootstrap_ci');
                fd.set('nonce', '<?php echo esc_js($nonce); ?>');
                try {
                    const resp = await fetch('<?php echo esc_js($ajax); ?>', { method:'POST', body:fd, credentials:'same-origin' });
                    const data = await resp.json();
                    if (data && data.success) {
                        status.style.color = '#15803d';
                        status.textContent = data.data.message || 'Created.';
                        setTimeout(() => window.location.reload(), 800);
                    } else {
                        status.style.color = '#b32d2e';
                        status.textContent = (data && data.data && data.data.message) || 'Failed.';
                        btn.disabled = false;
                    }
                } catch (e) {
                    status.style.color = '#b32d2e';
                    status.textContent = String(e);
                    btn.disabled = false;
                }
            });
        })();
        </script>
        <?php
        return ob_get_clean();
    }

    
    private function render_pipeline_card($tbl_sr, $tbl_si) {
        global $wpdb;
        $f18_total      = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_sr} WHERE form_id = 18");
        $f20_total      = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_sr} WHERE form_id = 20");
        $unresolved     = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_sr} WHERE eponymous_id = 0");
        $linked         = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_sr} WHERE eponymous_id > 0");
        $linked_unique  = (int) $wpdb->get_var("SELECT COUNT(DISTINCT eponymous_id) FROM {$tbl_sr} WHERE eponymous_id > 0");
        $last_ingest    = (string) $wpdb->get_var("SELECT MAX(ingested_at) FROM {$tbl_sr}");
        $next_cron_ts   = wp_next_scheduled(self::CRON_INGEST_HOOK);

        echo '<div class="aieo-dmm-card">';
        echo '<h3>' . esc_html__('② Ingestion pipeline — mirror Gravity Forms', 'ai-eshop-optimizer') . '</h3>';
        echo '<div class="aieo-attr-tiles">';
        $this->tile(__('F18 mirrored', 'ai-eshop-optimizer'),  number_format_i18n($f18_total),
            __('Total Gravity Forms F18 (post-order) entries copied into wp_aieo_survey_responses. Should match GF\'s own F18 entry count once the next sync completes.', 'ai-eshop-optimizer'));
        $this->tile(__('F20 mirrored', 'ai-eshop-optimizer'),  number_format_i18n($f20_total),
            __('Total Gravity Forms F20 (post-delivery) entries mirrored. F20 fires after a parcel is delivered and adds satisfaction + will-repurchase signal on top of F18.', 'ai-eshop-optimizer'));
        $this->tile(__('Linked customers', 'ai-eshop-optimizer'), number_format_i18n($linked_unique),
            __('Distinct eponymous_id values across all mirrored responses — i.e. how many unique customers have submitted at least one survey. Each customer can submit F18 and/or F20 multiple times.', 'ai-eshop-optimizer'));
        $this->tile(__('Unresolved emails', 'ai-eshop-optimizer'), number_format_i18n($unresolved),
            __('Mirrored responses whose email did NOT match any wp_aieo_customer_intelligence row at ingest time (eponymous_id = 0). Most are typos or guest checkouts; the next ingest cycle retries automatically.', 'ai-eshop-optimizer'));
        $this->tile(__('Last ingest', 'ai-eshop-optimizer'),
            $last_ingest ? esc_html(human_time_diff(strtotime($last_ingest . ' UTC'))) . ' ' . esc_html__('ago', 'ai-eshop-optimizer') : '—',
            __('Most recent time the ingest pipeline wrote a row into wp_aieo_survey_responses. Live submits update this immediately; scheduled cron at 03:00 Athens covers any drift.', 'ai-eshop-optimizer'));
        $this->tile(__('Next scheduled', 'ai-eshop-optimizer'),
            $next_cron_ts ? esc_html(human_time_diff(time(), $next_cron_ts)) : '—',
            __('Time until the next nightly ingest cron (aieo_surveys_ingest_tick) fires. Default schedule is daily 03:00 Athens.', 'ai-eshop-optimizer'));
        echo '</div>';

        echo '<p style="margin-top:10px;font-size:12px;color:#646970;">'
            . esc_html__('Responses are mirrored live on every form submit via gform_after_submission, and re-synced nightly at 03:00 Athens. Unresolved rows are entries whose email is not yet in customer_intelligence — they get retried automatically each ingest cycle.', 'ai-eshop-optimizer')
            . '</p>';

        echo '<div style="margin-top:14px;">';
        echo '<button type="button" class="button" id="aieo-surveys-ingest">';
        echo esc_html__('Ingest now', 'ai-eshop-optimizer');
        echo '</button>';
        echo ' <span id="aieo-surveys-ingest-status" style="margin-left:10px;color:#646970;font-size:13px;"></span>';
        echo '</div>';
        echo $this->ingest_inline_js(); 
        echo '</div>';
    } 

    private function render_recompute_card($tbl_si) {
        global $wpdb;
        $rows_total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_si}");
        $with_f18   = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_si} WHERE f18_entry_id > 0");
        $with_f20   = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_si} WHERE f20_entry_id > 0");
        $last       = (string) $wpdb->get_var("SELECT MAX(updated_at) FROM {$tbl_si}");

        echo '<div class="aieo-dmm-card">';
        echo '<h3>' . esc_html__('③ Customer Intelligence build — merge orders + surveys', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description" style="margin:-4px 0 12px;font-size:12px;">'
            . esc_html__('Recompute runs the full CDP pipeline: purchase metrics → discount/price → segments → position signals → core needs → TNP engagement → taste vectors → priority inference. The "with F18 / F20" tiles count voice-of-customer overlay; the rest of the intelligence builds even with zero questionnaires.', 'ai-eshop-optimizer')
            . '</p>';
        echo '<div class="aieo-attr-tiles">';
        $this->tile(__('Profiles with survey data', 'ai-eshop-optimizer'), number_format_i18n($rows_total),
            __('Total rows in wp_aieo_survey_intelligence — one per customer who responded to at least one questionnaire (F18 or F20). The denominator for "% F18 / F20 coverage".', 'ai-eshop-optimizer'));
        $this->tile(__('… with F18 (post-order)', 'ai-eshop-optimizer'), number_format_i18n($with_f18),
            __('Subset of those profiles that have at least one F18 entry (the post-order survey). F18 captures NPS + 7 priority dimensions and seeds priority inference.', 'ai-eshop-optimizer'));
        $this->tile(__('… with F20 (post-delivery)', 'ai-eshop-optimizer'), number_format_i18n($with_f20),
            __('Subset that have at least one F20 entry (post-delivery survey). F20 adds Overall satisfaction + Site UX + Will-repurchase, completing the VoC composite.', 'ai-eshop-optimizer'));
        $this->tile(__('Last build', 'ai-eshop-optimizer'),
            $last ? esc_html(human_time_diff(strtotime($last . ' UTC'))) . ' ' . esc_html__('ago', 'ai-eshop-optimizer') : '—',
            __('Most recent time "Recompute now" ran or the nightly cron updated wp_aieo_survey_intelligence. Stale = older than ~24h, since cron runs daily 02:30 Athens.', 'ai-eshop-optimizer'));
        echo '</div>';
        echo '<div style="margin-top:14px;">';
        if (current_user_can('manage_options')) {
            echo '<button type="button" class="button button-primary" id="aieo-surveys-recompute">';
            echo esc_html__('Recompute now', 'ai-eshop-optimizer');
            echo '</button>';
            echo ' <span id="aieo-surveys-recompute-status" style="margin-left:10px;color:#646970;font-size:13px;"></span>';
        } else {
            
            
            
            
            echo '<p class="description" style="margin:0;color:#9ca3af;font-size:12px;">'
                . esc_html__('Recompute is administrator-only — it runs the full 8-stage CDP pipeline and takes several minutes. The nightly cron at 02:30 Athens refreshes the data automatically.', 'ai-eshop-optimizer')
                . '</p>';
        }
        echo '</div>';
        echo $this->recompute_inline_js(); 
        echo '</div>';
    } 

    private function render_voc_distribution_card($tbl_si) {
        global $wpdb;
        $r = $wpdb->get_row("SELECT
            COUNT(*) total,
            ROUND(AVG(voc_score),1) avg_voc,
            SUM(CASE WHEN voc_band = 'high' THEN 1 ELSE 0 END) high_band,
            SUM(CASE WHEN voc_band = 'mid'  THEN 1 ELSE 0 END) mid_band,
            SUM(CASE WHEN voc_band = 'low'  THEN 1 ELSE 0 END) low_band
            FROM {$tbl_si} WHERE voc_score > 0", ARRAY_A);
        if (!$r || (int) $r['total'] === 0) {
            return;
        }

        echo '<div class="aieo-dmm-card">';
        echo '<h3>' . esc_html__('VoC composite score', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description" style="margin:0 0 8px;font-size:12px;">'
            . esc_html__('VoC = 40% NPS + 30% Overall satisfaction + 20% Site-UX avg + 10% Will-repurchase (when F20 present). The triggers and recommendation engine read this directly: high-VoC promoters become referral targets; low-VoC customers route to human service before any automated win-back.', 'ai-eshop-optimizer')
            . '</p>';
        echo '<div class="aieo-attr-tiles">';
        $this->tile(__('Avg VoC', 'ai-eshop-optimizer'), number_format_i18n((float) $r['avg_voc'], 1) . ' / 100',
            __('Mean VoC composite across all responders. VoC = 40% NPS + 30% Overall-Satisfaction + 20% Site-UX + 10% Will-Repurchase. Above 75 is healthy; below 60 signals a service problem.', 'ai-eshop-optimizer'));
        $this->tile(__('High-VoC (≥80)', 'ai-eshop-optimizer'), number_format_i18n((int) $r['high_band']),
            __('Customers with VoC ≥ 80 — the "advocates" cohort. Use these as the seed audience for referral programs, lookalike modelling, and review requests.', 'ai-eshop-optimizer'));
        $this->tile(__('Mid-VoC (50-79)', 'ai-eshop-optimizer'), number_format_i18n((int) $r['mid_band']),
            __('Customers with VoC 50–79 — neutral / improvable. Standard nurture audience: regular newsletter, mainstream offers.', 'ai-eshop-optimizer'));
        $this->tile(__('Low-VoC (<50)', 'ai-eshop-optimizer'), number_format_i18n((int) $r['low_band']),
            __('Customers with VoC < 50 — service-recovery cohort. Suppress these from automated win-back triggers and route to a human CS agent first.', 'ai-eshop-optimizer'));
        echo '</div></div>';
    }

    private function render_nps_distribution_card($tbl_si) {
        global $wpdb;
        $r = $wpdb->get_row("SELECT
            SUM(CASE WHEN nps_class = 'promoter'  THEN 1 ELSE 0 END) promoters,
            SUM(CASE WHEN nps_class = 'passive'   THEN 1 ELSE 0 END) passives,
            SUM(CASE WHEN nps_class = 'detractor' THEN 1 ELSE 0 END) detractors,
            COUNT(*) total
            FROM {$tbl_si} WHERE nps_class <> ''", ARRAY_A);
        if (!$r || (int) $r['total'] === 0) { return; }

        $promoters  = (int) $r['promoters'];
        $passives   = (int) $r['passives'];
        $detractors = (int) $r['detractors'];
        $total      = (int) $r['total'];
        $nps = $total > 0 ? round(100 * ($promoters - $detractors) / $total, 1) : 0;

        echo '<div class="aieo-dmm-card">';
        echo '<h3>' . esc_html__('NPS — Net Promoter Score', 'ai-eshop-optimizer') . '</h3>';
        echo '<div class="aieo-attr-tiles">';
        $this->tile(__('NPS', 'ai-eshop-optimizer'), ($nps >= 0 ? '+' : '') . number_format_i18n($nps, 1),
            __('Net Promoter Score = (% Promoters − % Detractors), range -100 to +100. Above +50 is excellent; +30–50 healthy; below 0 means more haters than fans.', 'ai-eshop-optimizer'));
        $this->tile(__('Promoters', 'ai-eshop-optimizer'), number_format_i18n($promoters) . ' (' . round(100 * $promoters / max(1,$total), 1) . '%)',
            __('Responders who scored 9 or 10 on "How likely are you to recommend us?" — your most enthusiastic customers, ideal seed for lookalike audiences and referral programs.', 'ai-eshop-optimizer'));
        $this->tile(__('Passives', 'ai-eshop-optimizer'),  number_format_i18n($passives)  . ' (' . round(100 * $passives  / max(1,$total), 1) . '%)',
            __('Responders who scored 7 or 8 — satisfied but not enthusiastic. Don\'t count toward NPS but are at-risk if a competitor offers something better.', 'ai-eshop-optimizer'));
        $this->tile(__('Detractors', 'ai-eshop-optimizer'), number_format_i18n($detractors) . ' (' . round(100 * $detractors / max(1,$total), 1) . '%)',
            __('Responders who scored 0–6. Suppressed from automated win-back / referral triggers and routed to human CS — automated nudging detractors makes them angrier.', 'ai-eshop-optimizer'));
        echo '</div>';
        echo '<p style="margin-top:8px;font-size:12px;color:#646970;">'
            . esc_html__('Detractors should be excluded from automated win-back triggers and routed to human service. Promoters are the natural seed for lookalike audiences and referral programs.', 'ai-eshop-optimizer')
            . '</p>';
        echo '</div>';
    }

    private function render_metric_distributions_card($tbl_si) {
        global $wpdb;
        echo '<div class="aieo-dmm-card">';
        echo '<h3>' . esc_html__('Seven high-signal metrics', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description" style="margin:0 0 10px;font-size:12px;">'
            . esc_html__('Each metric extracted from the survey responses. These flow directly into segment definitions and trigger filters.', 'ai-eshop-optimizer') . '</p>';

        echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:18px;">';

        echo '<div>';
        echo '<h4 style="margin:0 0 6px;font-size:13px;">' . esc_html__('Overall satisfaction (1–5)', 'ai-eshop-optimizer') . '</h4>';
        $rows = $wpdb->get_results("SELECT overall_satisfaction v, COUNT(*) c FROM {$tbl_si} WHERE overall_satisfaction > 0 GROUP BY overall_satisfaction ORDER BY overall_satisfaction", ARRAY_A);
        $this->mini_bar_chart($rows, '#15803d');
        echo '</div>';

        echo '<div>';
        echo '<h4 style="margin:0 0 6px;font-size:13px;">' . esc_html__('Professional / student status (verified)', 'ai-eshop-optimizer') . '</h4>';
        $rows = $wpdb->get_results("SELECT
            CASE WHEN is_professional_verified=1 THEN 'Yes' ELSE 'No' END v,
            COUNT(*) c FROM {$tbl_si} WHERE f18_entry_id > 0 GROUP BY is_professional_verified ORDER BY is_professional_verified DESC", ARRAY_A);
        $this->mini_bar_chart($rows, '#7c3aed');
        echo '</div>';

        echo '<div>';
        echo '<h4 style="margin:0 0 6px;font-size:13px;">' . esc_html__('Discovery channel', 'ai-eshop-optimizer') . '</h4>';
        $rows = $wpdb->get_results("SELECT discovery_channel v, COUNT(*) c FROM {$tbl_si} WHERE discovery_channel <> '' GROUP BY discovery_channel ORDER BY c DESC", ARRAY_A);
        $this->mini_bar_chart($rows, '#0891b2');
        echo '</div>';

        echo '<div>';
        echo '<h4 style="margin:0 0 6px;font-size:13px;">' . esc_html__('Engagement signals (F18)', 'ai-eshop-optimizer') . '</h4>';
        $rows = $wpdb->get_results("SELECT v, c FROM (
            SELECT 'Seminar attendee' v, SUM(engagement_seminar)    c FROM {$tbl_si} WHERE f18_entry_id > 0
            UNION ALL
            SELECT 'Blog reader',          SUM(engagement_blog)     FROM {$tbl_si} WHERE f18_entry_id > 0
            UNION ALL
            SELECT 'FastFinder user',      SUM(engagement_fastfinder) FROM {$tbl_si} WHERE f18_entry_id > 0
            UNION ALL
            SELECT 'Social login',         SUM(social_login)        FROM {$tbl_si} WHERE f18_entry_id > 0
        ) x ORDER BY c DESC", ARRAY_A);
        $this->mini_bar_chart($rows, '#ea580c');
        echo '</div>';

        echo '<div>';
        echo '<h4 style="margin:0 0 6px;font-size:13px;">' . esc_html__('Shipping price concern (F18)', 'ai-eshop-optimizer') . '</h4>';
        $rows = $wpdb->get_results("SELECT
            CASE WHEN shipping_price_concern=1 THEN 'Concerned' ELSE 'Not flagged' END v,
            COUNT(*) c FROM {$tbl_si} WHERE f18_entry_id > 0 GROUP BY shipping_price_concern ORDER BY shipping_price_concern DESC", ARRAY_A);
        $this->mini_bar_chart($rows, '#dc2626');
        echo '</div>';

        echo '<div>';
        echo '<h4 style="margin:0 0 6px;font-size:13px;">' . esc_html__('Site UX avg (rounded)', 'ai-eshop-optimizer') . '</h4>';
        $rows = $wpdb->get_results("SELECT FLOOR(site_ux_avg) v, COUNT(*) c FROM {$tbl_si} WHERE site_ux_avg > 0 GROUP BY FLOOR(site_ux_avg) ORDER BY FLOOR(site_ux_avg)", ARRAY_A);
        $this->mini_bar_chart($rows, '#0d9488');
        echo '</div>';

        echo '</div>';
        echo '</div>';
    }

    private function render_actionable_lists_card($tbl_si) {
        global $wpdb;
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';

        
        
        $ci_exists = (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl_ci)) === $tbl_ci;
        if (!$ci_exists) { return; }

        echo '<div class="aieo-dmm-section">';
        echo '<div class="aieo-dmm-card">';
        echo '<h3>' . esc_html__('Actionable lists', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description" style="margin:0 0 10px;font-size:12px;">'
            . esc_html__('The three lists worth a personal touch from your team — high-LTV detractors (rescue), high-VoC promoters (referrals), and customers who explicitly opted into the post-delivery follow-up.', 'ai-eshop-optimizer')
            . '</p>';

        echo '<h4 style="margin:14px 0 6px;font-size:13px;">' . esc_html__('Detractors with high LTV (rescue priority)', 'ai-eshop-optimizer') . '</h4>';
        $rows = $wpdb->get_results(
            "SELECT si.email, ci.ltv_total, si.nps_score, si.overall_satisfaction, si.voc_score, si.open_feedback_f18
             FROM {$tbl_si} si
             JOIN {$tbl_ci} ci ON ci.eponymous_id = si.eponymous_id
             WHERE si.nps_class = 'detractor' AND ci.ltv_total > 100
             ORDER BY ci.ltv_total DESC LIMIT 10",
            ARRAY_A
        );
        $this->actionable_table($rows, true);

        echo '<h4 style="margin:18px 0 6px;font-size:13px;">' . esc_html__('Top promoters (referral candidates)', 'ai-eshop-optimizer') . '</h4>';
        $rows = $wpdb->get_results(
            "SELECT si.email, ci.ltv_total, si.nps_score, si.overall_satisfaction, si.voc_score, si.open_feedback_f18
             FROM {$tbl_si} si
             JOIN {$tbl_ci} ci ON ci.eponymous_id = si.eponymous_id
             WHERE si.nps_class = 'promoter' AND si.voc_score >= 90
             ORDER BY ci.ltv_total DESC LIMIT 10",
            ARRAY_A
        );
        $this->actionable_table($rows, false);

        $f20_willing = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_si} WHERE will_take_followup_survey = 1");
        if ($f20_willing > 0) {
            echo '<h4 style="margin:18px 0 6px;font-size:13px;">' . esc_html__('F20 follow-up willing', 'ai-eshop-optimizer') . '</h4>';
            echo '<p style="margin:0;font-size:13px;color:#374151;">'
                . sprintf(
                    
                    esc_html__('%d customers explicitly opted into the post-delivery survey on Form 18. When the F20 trigger lands (Phase 5+), this is the seed audience to target — they\'ve given consent.', 'ai-eshop-optimizer'),
                    $f20_willing 
                )
                . '</p>'; 
        }
        echo '</div></div>';
    }

    
    private function get_f18_priority_map() {
        $cache_key = 'aieo_surveys_f18_priority_map';
        $cached = get_transient($cache_key);
        if (is_array($cached) && !empty($cached)) {
            return $cached;
        }

        global $wpdb;
        $tbl_meta = $wpdb->prefix . 'gf_form_meta';
        $json = (string) $wpdb->get_var($wpdb->prepare(
            "SELECT display_meta FROM {$tbl_meta} WHERE form_id = %d LIMIT 1",
            self::FORM_POST_ORDER
        ));
        if ($json === '') { return array(); }

        $form = json_decode($json, true);
        if (!is_array($form) || !isset($form['fields']) || !is_array($form['fields'])) {
            return array();
        }

        $map = array();
        foreach ($form['fields'] as $field) {
            if (!is_array($field)) { continue; }
            if ((string) ($field['id'] ?? '') !== '8') { continue; }
            if (!isset($field['choices']) || !is_array($field['choices'])) { break; }
            foreach ($field['choices'] as $choice) {
                $value = (string) ($choice['value'] ?? '');
                $text  = (string) ($choice['text']  ?? '');
                if ($value !== '' && $text !== '') {
                    $map[$value] = $text;
                }
            }
            break;
        }

        set_transient($cache_key, $map, DAY_IN_SECONDS);
        return $map;
    }

    
    private function render_priority_distribution_card($tbl_si) {
        global $wpdb;

        $map = $this->get_f18_priority_map();
        if (empty($map)) { return; }

        $rows = $wpdb->get_col(
            "SELECT personal_priorities FROM {$tbl_si}
             WHERE personal_priorities IS NOT NULL AND personal_priorities <> ''"
        );
        if (empty($rows)) { return; }

        
        
        
        
        
        $default_order_csv = implode(',', array_keys($map));

        $count_total       = count($rows);
        $count_default     = 0;
        $rank1_counts = array();
        $rank_sum     = array();
        $rank_n       = array();
        foreach ($map as $g => $_) {
            $rank1_counts[$g] = 0;
            $rank_sum[$g]     = 0;
            $rank_n[$g]       = 0;
        }

        foreach ($rows as $raw) {
            $cleaned = preg_replace('/\s+/', '', (string) $raw);
            if ($cleaned === $default_order_csv) {
                $count_default++;
                continue;
            }
            $ids = array_map('trim', explode(',', (string) $raw));
            $rank = 0;
            foreach ($ids as $g) {
                if ($g === '' || !isset($map[$g])) { continue; }
                $rank++;
                if ($rank === 1) { $rank1_counts[$g]++; }
                $rank_sum[$g] += $rank;
                $rank_n[$g]++;
            }
        }
        $count_engaged = $count_total - $count_default;

        $aggregate = array();
        foreach ($map as $g => $label) {
            $avg = $rank_n[$g] > 0 ? $rank_sum[$g] / $rank_n[$g] : 0;
            $aggregate[] = array(
                'label'     => $label,
                'avg'       => round($avg, 2),
                'mentions'  => $rank_n[$g],
                'rank1'     => $rank1_counts[$g],
                'rank1_pct' => $count_engaged > 0 ? round(100 * $rank1_counts[$g] / $count_engaged, 1) : 0,
            );
        }
        
        usort($aggregate, function($a, $b) {
            if ($a['avg'] === 0 && $b['avg'] === 0) { return 0; }
            if ($a['avg'] === 0) { return 1; }
            if ($b['avg'] === 0) { return -1; }
            return $a['avg'] <=> $b['avg'];
        });

        $worst_avg = 0;
        $best_rank1 = 0;
        foreach ($aggregate as $r) {
            if ($r['avg'] > $worst_avg) { $worst_avg = $r['avg']; }
            if ($r['rank1'] > $best_rank1) { $best_rank1 = $r['rank1']; }
        }

        $default_pct = $count_total > 0 ? round(100 * $count_default / $count_total, 1) : 0;

        echo '<div class="aieo-dmm-card">';
        echo '<h3>' . esc_html__('Customer priorities (F18 Q8)', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description" style="margin:0 0 8px;font-size:12px;">'
            . sprintf(
                
                esc_html__('Aggregated across %1$d engaged responders (excluded %2$d / %3$s%% who left the GF default rank order untouched — that is disengagement, not a ranking signal). Teal bar = average rank position (longer = more important on average); purple bar = how often the priority lands at rank #1 specifically. A priority that is consistently in everyone\'s top 3 dominates the teal bar even when it rarely wins #1 against a polarising favourite.', 'ai-eshop-optimizer'),
                $count_engaged, 
                $count_default, 
                $default_pct 
            ) 
            . '</p>'; 

        
        
        $default_labels = array_values($map);
        echo '<p style="margin:0 0 14px;font-size:11px;color:#6b7280;">';
        echo '<strong>' . esc_html__('Default order excluded:', 'ai-eshop-optimizer') . '</strong> ';
        echo esc_html(implode(' → ', $default_labels));
        echo '</p>';

        if ($count_engaged === 0) {
            echo '<p style="margin:0;color:#b32d2e;font-size:13px;">'
                . esc_html__('Every responder left the default order — no ranking signal to aggregate.', 'ai-eshop-optimizer')
                . '</p></div>';
            return;
        }

        echo '<table class="widefat" style="font-size:13px;">';
        echo '<thead><tr>';
        echo '<th style="width:30px;">#</th>';
        echo '<th>' . esc_html__('Priority', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:90px;">' . esc_html__('Avg rank', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:38%;">' . esc_html__('Importance bar (lower avg = more important)', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:90px;">' . esc_html__('Times #1', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:25%;">' . esc_html__('Rank-#1 share', 'ai-eshop-optimizer') . '</th>';
        echo '</tr></thead><tbody>';

        $i = 0;
        foreach ($aggregate as $r) {
            $i++;
            
            $importance_w = ($worst_avg > 1)
                ? round(100 * (1 - ($r['avg'] - 1) / max(0.0001, $worst_avg - 1)), 1)
                : 100;
            $rank1_w = $best_rank1 > 0 ? round(100 * $r['rank1'] / $best_rank1, 1) : 0;

            echo '<tr>';
            echo '<td style="color:#9ca3af;font-weight:600;">' . (int) $i . '</td>';
            echo '<td><strong>' . esc_html($r['label']) . '</strong></td>';
            echo '<td style="font-weight:600;color:#0d9488;">' . esc_html(number_format_i18n($r['avg'], 2)) . '</td>';
            echo '<td><div style="background:#f3f4f6;border-radius:4px;overflow:hidden;height:18px;">';
            echo '<div style="height:18px;background:#0d9488;width:' . esc_attr($importance_w) . '%;"></div>';
            echo '</div></td>';
            echo '<td>' . esc_html(number_format_i18n($r['rank1'])) . ' <small style="color:#6b7280;">(' . $r['rank1_pct'] . '%)</small></td>'; 
            echo '<td><div style="background:#f3f4f6;border-radius:4px;overflow:hidden;height:18px;">';
            echo '<div style="height:18px;background:#7c3aed;width:' . esc_attr($rank1_w) . '%;"></div>'; 
            echo '</div></td>';
            echo '</tr>';
        }
        echo '</tbody></table>';

        echo '</div>';
    }

    
    private function render_inferred_priorities_card() {
        global $wpdb;
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';
        $tbl_si = $wpdb->prefix . 'aieo_survey_intelligence';

        
        
        $ci_exists = (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl_ci)) === $tbl_ci;
        if (!$ci_exists) {
            echo '<div class="aieo-dmm-card" style="background:#fef2f2;border-left:3px solid #b32d2e;">';
            echo '<h3>' . esc_html__('④ Inferred priorities — Tier 1 heuristic', 'ai-eshop-optimizer') . '</h3>';
            echo '<p style="margin:0;font-size:13px;color:#7f1d1d;">'
                . esc_html__('Cannot run: wp_aieo_customer_intelligence table does not exist. That table is owned by the Web Push module (Phase 1) and is the foundation for every CDP feature in here. Open the Web Push tab, run its recompute once, then come back here and re-press Recompute on this tab.', 'ai-eshop-optimizer')
                . '</p>';
            echo '</div>';
            return;
        }

        
        $dist = $wpdb->get_results(
            "SELECT inferred_top_priority slug,
                    COUNT(*) c,
                    ROUND(AVG(inferred_priorities_confidence), 2) avg_conf
             FROM {$tbl_ci}
             WHERE inferred_top_priority <> ''
             GROUP BY inferred_top_priority
             ORDER BY c DESC",
            ARRAY_A
        );

        $taxonomy = self::priority_taxonomy();
        $total_inferred = 0;
        foreach ($dist as $r) { $total_inferred += (int) $r['c']; }

        echo '<div class="aieo-dmm-card">';
        echo '<h3>' . esc_html__('④ Inferred priorities — Tier 1 heuristic', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description" style="margin:0 0 14px;font-size:12px;">'
            . esc_html__('Rule-based extrapolation from F18 responders\' priorities to the full customer base via existing CI scalars (discount_share, price_band, top_brand_share, segments). Single SQL UPDATE — runs in seconds for ~33k rows. Top priority drives newsletter content branching + personalized coupon offer type. Confidence (0..1) = top-score / sum-of-scores; low values mean the heuristic had no clear winner and the customer fell to "general".', 'ai-eshop-optimizer')
            . '</p>';

        if ($total_inferred === 0) {
            echo '<p style="margin:0;padding:10px 14px;background:#fffbeb;border-left:3px solid #d97706;font-size:13px;color:#78350f;">'
                . esc_html__('No inferred priorities yet — press "Recompute now" to run the heuristic across customer_intelligence.', 'ai-eshop-optimizer')
                . '</p></div>';
            return;
        }

        echo '<h4 style="margin:14px 0 6px;font-size:13px;">' . esc_html__('Distribution across full base', 'ai-eshop-optimizer') . '</h4>';
        echo '<table class="widefat striped" style="font-size:12px;">';
        echo '<thead><tr>';
        echo '<th>' . esc_html__('Slug', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('Greek label', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('Customers', 'ai-eshop-optimizer') . '</th>';
        echo '<th style="width:35%;"></th>';
        echo '<th>' . esc_html__('Share', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('Avg confidence', 'ai-eshop-optimizer') . '</th>';
        echo '</tr></thead><tbody>';
        $max_c = $dist ? max(array_column($dist, 'c')) : 1;
        foreach ($dist as $r) {
            $slug = (string) $r['slug'];
            $label = isset($taxonomy[$slug]) ? $taxonomy[$slug] : $slug;
            $share = round(100 * (int) $r['c'] / max(1, $total_inferred), 1);
            $w = round(100 * (int) $r['c'] / max(1, $max_c), 1);
            echo '<tr>';
            echo '<td><code>' . esc_html($slug) . '</code></td>';
            echo '<td>' . esc_html($label) . '</td>';
            echo '<td>' . esc_html(number_format_i18n((int) $r['c'])) . '</td>';
            echo '<td><div style="background:#f3f4f6;border-radius:3px;overflow:hidden;height:14px;"><div style="height:14px;background:#0d9488;width:' . esc_attr($w) . '%;"></div></div></td>';
            echo '<td>' . $share . '%</td>'; 
            echo '<td>' . esc_html(number_format_i18n((float) $r['avg_conf'], 2)) . '</td>';
            echo '</tr>'; 
        }
        echo '</tbody></table>';

        
        
        
        
        
        
        
        
        $current_source = (string) $wpdb->get_var(
            "SELECT inferred_priorities_source FROM {$tbl_ci}
             WHERE inferred_priorities_source <> '' LIMIT 1"
        );

        if (strpos($current_source, 'knn') === 0) {
            
            $loo = $this->validate_knn_leave_one_out();
            if (($loo['total'] ?? 0) > 0) {
                echo '<h4 style="margin:18px 0 6px;font-size:13px;">' . esc_html__('Tier 2 accuracy — leave-one-out on F18 responders', 'ai-eshop-optimizer') . '</h4>';
                echo '<p class="description" style="margin:0 0 8px;font-size:12px;">'
                    . sprintf(
                        
                        esc_html__('For each of %d engaged F18 responders, we predict their priority using the OTHER labelled responders (excluding themselves) and compare against their explicit answer. This is the honest measurement of the k-NN propagation quality on a held-out set — no self-validation cheating.', 'ai-eshop-optimizer'),
                        (int) $loo['total']
                    )
                    . '</p>';
                echo '<div class="aieo-attr-tiles">';
                $this->tile(__('Considered', 'ai-eshop-optimizer'), number_format_i18n((int) $loo['total']),
                    __('Number of F18 responders included in the leave-one-out evaluation. We predict each one using the OTHER labelled responders, then compare to their explicit answer.', 'ai-eshop-optimizer'));
                $this->tile(__('k-NN top = their top', 'ai-eshop-optimizer'),
                    number_format_i18n((int) $loo['top1']) . ' (' . $loo['top1_pct'] . '%)',
                    __('How often the k-NN-predicted top priority exactly matched what the responder explicitly chose as #1. Strict accuracy. Random-baseline = 11% (1 of 9 priorities).', 'ai-eshop-optimizer'));
                $this->tile(__('k-NN top in their top-3', 'ai-eshop-optimizer'),
                    number_format_i18n((int) $loo['top3']) . ' (' . $loo['top3_pct'] . '%)',
                    __('How often the k-NN-predicted top priority appears in the responder\'s own top-3. Soft-match. Random-baseline ≈ 33%; >50% beats chance; >70% is newsletter-worthy quality.', 'ai-eshop-optimizer'));
                echo '</div>';
                echo '<p style="margin-top:8px;font-size:12px;color:#646970;">'
                    . esc_html__('Top-3 hit rate against random 9-choice baseline (33%): >50% = better than chance; >70% = newsletter-worthy. If well below 50%, the feature vector is missing the signal that drives priority — usually engagement / needs profile.', 'ai-eshop-optimizer')
                    . '</p>';
            }
        } else {
            
            $f18_rows = $wpdb->get_results(
                "SELECT si.eponymous_id, si.personal_priorities, ci.inferred_top_priority
                 FROM {$tbl_si} si
                 JOIN {$tbl_ci} ci ON ci.eponymous_id = si.eponymous_id
                 WHERE si.f18_entry_id > 0
                   AND si.personal_priorities IS NOT NULL AND si.personal_priorities <> ''
                   AND ci.inferred_top_priority <> ''",
                ARRAY_A
            );

            if (!empty($f18_rows)) {
                $grank_to_label = $this->get_f18_priority_map();
                $default_csv    = implode(',', array_keys($grank_to_label));
                $label_to_slug  = $this->build_label_to_slug_map($grank_to_label);

                $matched_top1 = 0; $matched_top3 = 0; $considered = 0;
                foreach ($f18_rows as $row) {
                    $raw = (string) $row['personal_priorities'];
                    $cleaned = preg_replace('/\s+/', '', $raw);
                    if ($cleaned === $default_csv) { continue; }
                    $considered++;

                    $slugs_in_order = array();
                    foreach (explode(',', $raw) as $g) {
                        $g = trim($g);
                        if (!isset($grank_to_label[$g])) { continue; }
                        $label = $grank_to_label[$g];
                        if (isset($label_to_slug[$label])) { $slugs_in_order[] = $label_to_slug[$label]; }
                    }
                    if (empty($slugs_in_order)) { continue; }

                    $top3_slugs = array_slice($slugs_in_order, 0, 3);
                    $inferred = (string) $row['inferred_top_priority'];
                    if ($inferred === $slugs_in_order[0]) { $matched_top1++; }
                    if (in_array($inferred, $top3_slugs, true)) { $matched_top3++; }
                }

                $top1_pct = $considered > 0 ? round(100 * $matched_top1 / $considered, 1) : 0;
                $top3_pct = $considered > 0 ? round(100 * $matched_top3 / $considered, 1) : 0;

                echo '<h4 style="margin:18px 0 6px;font-size:13px;">' . esc_html__('Heuristic accuracy on F18 hold-out', 'ai-eshop-optimizer') . '</h4>';
                echo '<p class="description" style="margin:0 0 8px;font-size:12px;">'
                    
                    . sprintf(esc_html__('Tested against %d engaged F18 responders (default-order skippers excluded).', 'ai-eshop-optimizer'), $considered) 
                    . '</p>';
                echo '<div class="aieo-attr-tiles">'; 
                $this->tile(__('Considered', 'ai-eshop-optimizer'), number_format_i18n($considered),
                    __('Number of F18 responders included in heuristic self-validation (those whose priority CSV is non-default and decodable into our 9 priority slugs).', 'ai-eshop-optimizer'));
                $this->tile(__('Heuristic top = their top', 'ai-eshop-optimizer'), number_format_i18n($matched_top1) . ' (' . $top1_pct . '%)',
                    __('How often the rule-based heuristic predicted the same #1 priority the responder explicitly chose. Strict accuracy on the heuristic Tier-1 inference path.', 'ai-eshop-optimizer'));
                $this->tile(__('Heuristic top in their top-3', 'ai-eshop-optimizer'), number_format_i18n($matched_top3) . ' (' . $top3_pct . '%)',
                    __('How often the heuristic prediction appears anywhere in the responder\'s top-3. Soft-match — used to gate when k-NN should override the heuristic.', 'ai-eshop-optimizer'));
                echo '</div>';
            }
        }

        echo '</div>';
    }

    
    private function render_position_diagnostics_card() {
        global $wpdb;
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';

        $ci_exists = (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl_ci)) === $tbl_ci;
        if (!$ci_exists) { return; }
        $total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_ci}");
        if ($total === 0) { return; }

        $row = $wpdb->get_row("SELECT
            -- sole_order_share buckets
            SUM(IF(sole_order_share = 0, 1, 0))                            AS sos_0,
            SUM(IF(sole_order_share > 0    AND sole_order_share < 0.25, 1, 0)) AS sos_low,
            SUM(IF(sole_order_share >= 0.25 AND sole_order_share < 0.50, 1, 0)) AS sos_mid,
            SUM(IF(sole_order_share >= 0.50 AND sole_order_share < 0.75, 1, 0)) AS sos_high,
            SUM(IF(sole_order_share >= 0.75, 1, 0))                        AS sos_max,
            -- avg_jlw buckets
            SUM(IF(avg_jlw > 0    AND avg_jlw < 0.30, 1, 0))               AS jlw_early,
            SUM(IF(avg_jlw >= 0.30 AND avg_jlw < 0.50, 1, 0))              AS jlw_lower,
            SUM(IF(avg_jlw >= 0.50 AND avg_jlw < 0.70, 1, 0))              AS jlw_upper,
            SUM(IF(avg_jlw >= 0.70, 1, 0))                                 AS jlw_late,
            SUM(IF(avg_jlw = 0, 1, 0))                                     AS jlw_unset,
            -- avg_vlw buckets
            SUM(IF(avg_vlw > 0    AND avg_vlw < 0.30, 1, 0))               AS vlw_addon,
            SUM(IF(avg_vlw >= 0.30 AND avg_vlw < 0.60, 1, 0))              AS vlw_mid,
            SUM(IF(avg_vlw >= 0.60 AND avg_vlw < 0.90, 1, 0))              AS vlw_dominant,
            SUM(IF(avg_vlw >= 0.90, 1, 0))                                 AS vlw_solo,
            SUM(IF(avg_vlw = 0, 1, 0))                                     AS vlw_unset,
            -- core_needs coverage
            SUM(IF(core_needs_json IS NOT NULL AND core_needs_json <> '' AND core_needs_json <> '{}', 1, 0)) AS needs_set
            FROM {$tbl_ci}", ARRAY_A);

        echo '<div class="aieo-dmm-card" style="background:#eff6ff;border-left:3px solid #1d4ed8;">';
        echo '<h3>' . esc_html__('Position-signal diagnostics — first / last / sole', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description" style="margin:0 0 14px;font-size:12px;color:#1e3a8a;">'
            . esc_html__('The AIEO core hypothesis: a customer\'s first / last / sole products encode their real expressed needs better than any aggregate scalar. Sole-order customers (single-item orders) know exactly what they want — they paid shipping just for THAT product. First-in-cart products are the magnet. Last-in-cart are the must-not-forgets. Below: how the population distributes on these axes.', 'ai-eshop-optimizer')
            . '</p>';

        echo '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:18px;">';

        
        echo '<div>';
        echo '<h4 style="margin:0 0 6px;font-size:13px;">' . esc_html__('Sole-order share (pure-intent share)', 'ai-eshop-optimizer') . '</h4>';
        $this->mini_dist_bar(array(
            '0% (always multi-item)' => (int) $row['sos_0'],
            '0.01-24%'              => (int) $row['sos_low'],
            '25-49%'                => (int) $row['sos_mid'],
            '50-74%'                => (int) $row['sos_high'],
            '75%+ (pure-intent)'    => (int) $row['sos_max'],
        ), $total, '#1d4ed8');
        echo '<p style="margin:6px 0 0;font-size:11px;color:#9ca3af;">'
            . esc_html__('High sole-order share = customer has specific known needs. Strongest cohort for need-targeted messaging.', 'ai-eshop-optimizer')
            . '</p>';
        echo '</div>';

        
        echo '<div>';
        echo '<h4 style="margin:0 0 6px;font-size:13px;">' . esc_html__('Avg journey weight (early vs late shopper)', 'ai-eshop-optimizer') . '</h4>';
        $this->mini_dist_bar(array(
            'unset'           => (int) $row['jlw_unset'],
            '0-0.29 (early)'  => (int) $row['jlw_early'],
            '0.30-0.49'       => (int) $row['jlw_lower'],
            '0.50-0.69'       => (int) $row['jlw_upper'],
            '0.70+ (late)'    => (int) $row['jlw_late'],
        ), $total, '#7c3aed');
        echo '<p style="margin:6px 0 0;font-size:11px;color:#9ca3af;">'
            . esc_html__('JLW = ItemSequence/OrderSize. 1.0 = always last in cart, 0 = always first. Sole-order customers cluster at 1.0.', 'ai-eshop-optimizer')
            . '</p>';
        echo '</div>';

        
        echo '<div>';
        echo '<h4 style="margin:0 0 6px;font-size:13px;">' . esc_html__('Avg value share (add-on vs headliner)', 'ai-eshop-optimizer') . '</h4>';
        $this->mini_dist_bar(array(
            'unset'                  => (int) $row['vlw_unset'],
            '0-0.29 (add-on)'        => (int) $row['vlw_addon'],
            '0.30-0.59'              => (int) $row['vlw_mid'],
            '0.60-0.89 (dominant)'   => (int) $row['vlw_dominant'],
            '0.90+ (solo product)'   => (int) $row['vlw_solo'],
        ), $total, '#0d9488');
        echo '<p style="margin:6px 0 0;font-size:11px;color:#9ca3af;">'
            . esc_html__('VLW = LineValue/OrderItemsTotal. 1.0 = product dominates the order value, 0 = small companion item.', 'ai-eshop-optimizer')
            . '</p>';
        echo '</div>';

        echo '</div>'; 

        
        $needs_set = (int) $row['needs_set'];
        $needs_pct = round(100 * $needs_set / max(1, $total), 1);
        echo '<p style="margin:14px 0 6px;font-size:13px;">';
        echo '<strong>' . esc_html__('core_needs_json coverage:', 'ai-eshop-optimizer') . '</strong> ';
        echo number_format_i18n($needs_set) . ' / ' . number_format_i18n($total) . ' customers (' . $needs_pct . '%) '; 
        if ($needs_pct < 50) {
            echo '<span style="color:#b91c1c;">' . esc_html__('— low coverage means many customers\' top products lack pa_need taxonomy assignments. Tagging more products with pa_need terms would meaningfully boost the k-NN cosine signal.', 'ai-eshop-optimizer') . '</span>'; 
        } elseif ($needs_pct < 80) {
            echo '<span style="color:#d97706;">' . esc_html__('— moderate coverage. The k-NN needs-cosine term contributes for these but falls back to scalar matching for the rest.', 'ai-eshop-optimizer') . '</span>';
        } else {
            echo '<span style="color:#15803d;">' . esc_html__('— strong coverage. Needs-cosine drives priority similarity for most customers.', 'ai-eshop-optimizer') . '</span>';
        }
        echo '</p>';

        
        echo '<h4 style="margin:18px 0 6px;font-size:13px;">' . esc_html__('Catalog role archetypes — most common top product across customers', 'ai-eshop-optimizer') . '</h4>';
        echo '<p style="margin:0 0 8px;font-size:12px;color:#374151;">'
            . esc_html__('For each role (sole, first, last) — which products show up most often as a customer\'s top product in that position. These are the catalog\'s natural role-occupants: products that AIEO\'s order-line tracking says are repeatedly chosen as magnets / completers / pure-intent destinations.', 'ai-eshop-optimizer')
            . '</p>';

        echo '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;">';
        foreach (array(
            array('column' => 'top_sole_product_id',  'label' => __('Top sole-order destinations', 'ai-eshop-optimizer'), 'color' => '#1d4ed8'),
            array('column' => 'top_first_product_id', 'label' => __('Top magnet products (first in cart)', 'ai-eshop-optimizer'), 'color' => '#7c3aed'),
            array('column' => 'top_last_product_id',  'label' => __('Top completion products (last in cart)', 'ai-eshop-optimizer'), 'color' => '#0d9488'),
        ) as $role) {
            $top = $wpdb->get_results(
                "SELECT ci.{$role['column']} AS product_id, COUNT(*) AS hits, p.post_title AS name
                 FROM {$tbl_ci} ci
                 LEFT JOIN {$wpdb->posts} p ON p.ID = ci.{$role['column']}
                 WHERE ci.{$role['column']} > 0
                 GROUP BY ci.{$role['column']}
                 ORDER BY hits DESC
                 LIMIT 10",
                ARRAY_A
            );
            echo '<div>';
            echo '<h5 style="margin:0 0 4px;font-size:12px;color:' . esc_attr($role['color']) . ';">' . esc_html($role['label']) . '</h5>';
            if (empty($top)) {
                echo '<p style="font-size:12px;color:#9ca3af;">' . esc_html__('No data yet.', 'ai-eshop-optimizer') . '</p>';
            } else {
                $max_hits = (int) $top[0]['hits'];
                echo '<table style="width:100%;font-size:11px;">';
                foreach ($top as $r) {
                    $w = $max_hits > 0 ? round(100 * (int) $r['hits'] / $max_hits, 1) : 0;
                    $name = !empty($r['name']) ? mb_substr((string) $r['name'], 0, 38) : '#' . (int) $r['product_id'];
                    echo '<tr>';
                    echo '<td style="color:#374151;padding:2px 4px;">' . esc_html($name) . '</td>';
                    echo '<td style="width:30%;padding:2px 4px;"><div style="background:#f3f4f6;border-radius:3px;overflow:hidden;height:10px;"><div style="height:10px;background:' . esc_attr($role['color']) . ';width:' . esc_attr($w) . '%;"></div></div></td>';
                    echo '<td style="width:60px;text-align:right;padding:2px 4px;color:#1f2937;">' . esc_html(number_format_i18n((int) $r['hits'])) . '</td>';
                    echo '</tr>';
                }
                echo '</table>';
            }
            echo '</div>';
        }
        echo '</div>'; 

        echo '</div>';
    }

    
    private function render_engagement_diagnostics_card() {
        global $wpdb;
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';
        if ((string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl_ci)) !== $tbl_ci) { return; }
        
        
        
        
        $total = (int) $wpdb->get_var("SELECT COUNT(DISTINCT IF(email <> '', email, NULL)) FROM {$tbl_ci}");
        if ($total === 0) { return; }

        
        
        
        
        
        
        
        
        
        $row = $wpdb->get_row("SELECT
            COUNT(DISTINCT IF(email <> '' AND tnp_subscriber_id > 0, email, NULL)) AS matched,
            COUNT(DISTINCT IF(email <> '' AND tnp_status = 'C', email, NULL))      AS confirmed,
            COUNT(DISTINCT IF(email <> '' AND tnp_status = 'U', email, NULL))      AS unsubscribed,
            COUNT(DISTINCT IF(email <> '' AND emails_sent_lifetime >= 3, email, NULL))   AS sent_3plus,
            COUNT(DISTINCT IF(email <> '' AND emails_opened_lifetime > 0, email, NULL))  AS ever_opened,
            COUNT(DISTINCT IF(email <> '' AND emails_clicked_lifetime > 0, email, NULL)) AS ever_clicked,
            COUNT(DISTINCT IF(email <> '' AND emails_opened_30d > 0, email, NULL))       AS opened_30d,
            -- Open-rate buckets (lifetime, restricted to 3+ sent so denominator meaningful)
            COUNT(DISTINCT IF(email <> '' AND emails_sent_lifetime >= 3 AND open_rate_lifetime = 0, email, NULL))                              AS or_zero,
            COUNT(DISTINCT IF(email <> '' AND emails_sent_lifetime >= 3 AND open_rate_lifetime > 0    AND open_rate_lifetime < 0.10, email, NULL))   AS or_low,
            COUNT(DISTINCT IF(email <> '' AND emails_sent_lifetime >= 3 AND open_rate_lifetime >= 0.10 AND open_rate_lifetime < 0.30, email, NULL)) AS or_mid,
            COUNT(DISTINCT IF(email <> '' AND emails_sent_lifetime >= 3 AND open_rate_lifetime >= 0.30 AND open_rate_lifetime < 0.50, email, NULL)) AS or_high,
            COUNT(DISTINCT IF(email <> '' AND emails_sent_lifetime >= 3 AND open_rate_lifetime >= 0.50, email, NULL))                          AS or_max,
            -- Click rate buckets
            COUNT(DISTINCT IF(email <> '' AND emails_sent_lifetime >= 3 AND click_rate_lifetime = 0, email, NULL))                             AS cr_zero,
            COUNT(DISTINCT IF(email <> '' AND emails_sent_lifetime >= 3 AND click_rate_lifetime > 0    AND click_rate_lifetime < 0.05, email, NULL))  AS cr_low,
            COUNT(DISTINCT IF(email <> '' AND emails_sent_lifetime >= 3 AND click_rate_lifetime >= 0.05 AND click_rate_lifetime < 0.15, email, NULL)) AS cr_mid,
            COUNT(DISTINCT IF(email <> '' AND emails_sent_lifetime >= 3 AND click_rate_lifetime >= 0.15, email, NULL))                         AS cr_high,
            -- Engagement recency buckets
            COUNT(DISTINCT IF(email <> '' AND days_since_last_engagement <= 30 AND days_since_last_engagement < 65535, email, NULL))      AS eng_30d,
            COUNT(DISTINCT IF(email <> '' AND days_since_last_engagement > 30   AND days_since_last_engagement <= 90, email, NULL))       AS eng_90d,
            COUNT(DISTINCT IF(email <> '' AND days_since_last_engagement > 90   AND days_since_last_engagement <= 365, email, NULL))      AS eng_year,
            COUNT(DISTINCT IF(email <> '' AND days_since_last_engagement > 365  AND days_since_last_engagement < 65535, email, NULL))     AS eng_old,
            COUNT(DISTINCT IF(email <> '' AND days_since_last_engagement = 65535, email, NULL))                                           AS eng_never
            FROM {$tbl_ci}", ARRAY_A);

        $matched = (int) $row['matched'];
        $match_pct = round(100 * $matched / max(1, $total), 1);

        echo '<div class="aieo-dmm-card" style="background:#fffbeb;border-left:3px solid #d97706;">';
        echo '<h3>' . esc_html__('Newsletter engagement (TNP) — match + reachability', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description" style="margin:0 0 14px;font-size:12px;color:#78350f;">'
            . esc_html__('Bridges customer email → wp_newsletter subscriber, then aggregates send / open / click history. Engagement signals feed the k-NN priority extrapolation (4 added dims) and inform letter-vs-coupon strategy: high open + low click = letter audience; high click = coupon audience.', 'ai-eshop-optimizer')
            . '</p>';

        
        
        $confirmed_n   = max(1, (int) $row['confirmed']);
        $opened_pct    = round(100 * ((int) $row['ever_opened'])  / $confirmed_n, 1);
        $clicked_pct   = round(100 * ((int) $row['ever_clicked']) / $confirmed_n, 1);
        $eng30_pct     = round(100 * ((int) $row['opened_30d'])   / $confirmed_n, 1);

        echo '<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:14px;margin-bottom:14px;">';
        $tiles = array(
            array(
                'label' => __('Matched to TNP', 'ai-eshop-optimizer'),
                'value' => number_format_i18n($matched),
                'sub'   => $match_pct . '% of CI',
                'help'  => __('Customer profiles whose email address also appears as a subscriber in TNP\'s wp_newsletter table. Match is case-insensitive. The "% of CI" is the share of all known customers (including those without an email) that we can reach via TNP — the other half are guests / pure-anonymous sessions.', 'ai-eshop-optimizer'),
            ),
            array(
                'label' => __('Confirmed (C)', 'ai-eshop-optimizer'),
                'value' => number_format_i18n((int) $row['confirmed']),
                'sub'   => __('reachable', 'ai-eshop-optimizer'),
                'help'  => __('Of the matched subscribers, those whose TNP status is "C" (confirmed double-opt-in). Only this cohort is legally and technically reachable for newsletter sends — unconfirmed / unsubscribed / bounced are excluded by TNP\'s own dispatcher.', 'ai-eshop-optimizer'),
            ),
            array(
                'label' => __('Ever opened', 'ai-eshop-optimizer'),
                'value' => number_format_i18n((int) $row['ever_opened']),
                'sub'   => $opened_pct . '% ' . __('of confirmed', 'ai-eshop-optimizer'),
                'help'  => __('Subscribers who have opened at least one newsletter in their entire history (lifetime, no recency cap). Percentage is against the Confirmed cohort — the only meaningful denominator since unconfirmed users never received any sends.', 'ai-eshop-optimizer'),
            ),
            array(
                'label' => __('Ever clicked', 'ai-eshop-optimizer'),
                'value' => number_format_i18n((int) $row['ever_clicked']),
                'sub'   => $clicked_pct . '% ' . __('of confirmed', 'ai-eshop-optimizer'),
                'help'  => __('Subscribers who clicked at least one link in any newsletter (lifetime). High-intent cohort: openers read, but clickers acted. This is the natural seed audience for promo / offer campaigns — they\'ve self-selected as buyers.', 'ai-eshop-optimizer'),
            ),
            array(
                'label' => __('Engaged in last 30d', 'ai-eshop-optimizer'),
                'value' => number_format_i18n((int) $row['opened_30d']),
                'sub'   => $eng30_pct . '% ' . __('opened ≥1 in 30d', 'ai-eshop-optimizer'),
                'help'  => __('Subscribers who opened a newsletter in the last 30 days. Strong reachability signal — these are the audience most likely to read what you send next. Use as the high-confidence cohort for time-sensitive campaigns.', 'ai-eshop-optimizer'),
            ),
        );
        foreach ($tiles as $t) {
            $help = isset($t['help']) ? (string) $t['help'] : '';
            echo '<div class="aieo-attr-tile" style="background:#fef3c7;padding:10px;border-radius:6px;position:relative;">';
            echo '<div class="aieo-attr-tile-label" style="font-size:11px;color:#92400e;">';
            echo '<span>' . esc_html($t['label']) . '</span>';
            if ($help !== '') {
                echo '<span class="aieo-attr-tile-help" style="background:#fde68a;color:#92400e;" aria-label="' . esc_attr($help) . '">i</span>';
            }
            echo '</div>';
            echo '<div style="font-size:22px;font-weight:600;color:#78350f;margin:4px 0 2px;">' . esc_html($t['value']) . '</div>';
            echo '<div style="font-size:11px;color:#a16207;">' . esc_html($t['sub']) . '</div>';
            if ($help !== '') {
                echo '<div class="aieo-attr-tile-popover">' . esc_html($help) . '</div>';
            }
            echo '</div>';
        }
        echo '</div>';

        $sent3 = max(1, (int) $row['sent_3plus']);
        echo '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:18px;">';

        echo '<div>';
        echo '<h4 style="margin:0 0 6px;font-size:13px;">' . esc_html__('Open rate (lifetime, 3+ sent)', 'ai-eshop-optimizer') . '</h4>';
        $this->mini_dist_bar(array(
            '0%'         => (int) $row['or_zero'],
            '0-9%'       => (int) $row['or_low'],
            '10-29%'     => (int) $row['or_mid'],
            '30-49%'     => (int) $row['or_high'],
            '50%+'       => (int) $row['or_max'],
        ), $sent3, '#d97706');
        echo '</div>';

        echo '<div>';
        echo '<h4 style="margin:0 0 6px;font-size:13px;">' . esc_html__('Click rate (lifetime, 3+ sent)', 'ai-eshop-optimizer') . '</h4>';
        $this->mini_dist_bar(array(
            '0%'         => (int) $row['cr_zero'],
            '0-4%'       => (int) $row['cr_low'],
            '5-14%'      => (int) $row['cr_mid'],
            '15%+'       => (int) $row['cr_high'],
        ), $sent3, '#b45309');
        echo '</div>';

        echo '<div>';
        echo '<h4 style="margin:0 0 6px;font-size:13px;">' . esc_html__('Engagement recency', 'ai-eshop-optimizer') . '</h4>';
        $this->mini_dist_bar(array(
            __('≤30 days', 'ai-eshop-optimizer')        => (int) $row['eng_30d'],
            __('31-90 days', 'ai-eshop-optimizer')      => (int) $row['eng_90d'],
            __('91-365 days', 'ai-eshop-optimizer')     => (int) $row['eng_year'],
            __('>365 days', 'ai-eshop-optimizer')       => (int) $row['eng_old'],
            __('Never', 'ai-eshop-optimizer')           => (int) $row['eng_never'],
        ), $total, '#92400e');
        echo '</div>';
        echo '</div>'; 

        echo '</div>'; 
    }

    
    private function render_push_engagement_diagnostics_card() {
        global $wpdb;
        $tbl_subs = $wpdb->prefix . 'aieo_push_subscriptions';
        $tbl_sess = $wpdb->prefix . 'aieo_sessions_statics';

        
        
        
        
        $has_subs = (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl_subs)) === $tbl_subs;
        $has_sess = (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl_sess)) === $tbl_sess;
        if (!$has_subs || !$has_sess) { return; }
        $has_channel_col = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s",
            DB_NAME, $tbl_sess, 'entry_channel'
        ));
        if ($has_channel_col === 0) { return; }

        
        
        
        
        $known_subs = (int) $wpdb->get_var(
            "SELECT COUNT(DISTINCT user_id) FROM {$tbl_subs}
             WHERE status = 'active' AND user_id > 0"
        );
        $anon_subs = (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM {$tbl_subs}
             WHERE status = 'active' AND user_id = 0"
        );
        $total_subs = $known_subs + $anon_subs;

        
        
        
        
        $ever_responded = (int) $wpdb->get_var(
            "SELECT COUNT(DISTINCT user_id) FROM {$tbl_sess}
             WHERE entry_channel = 'web_push' AND user_id > 0"
        );
        
        
        $col_for_recency = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s",
            DB_NAME, $tbl_sess, 'created_at'
        )) > 0 ? 'created_at' : 'last_activity';
        $responded_30d = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(DISTINCT user_id) FROM {$tbl_sess}
             WHERE entry_channel = 'web_push' AND user_id > 0
               AND {$col_for_recency} >= %s",
            gmdate('Y-m-d H:i:s', strtotime('-30 days'))
        ));

        
        
        
        
        $denom = max(1, $known_subs);
        $response_rate    = round(100 * $ever_responded / $denom, 1);
        $recent_rate      = round(100 * $responded_30d  / $denom, 1);

        echo '<div class="aieo-dmm-card" style="background:#f5f3ff;border-left:3px solid #7c3aed;">';
        echo '<h3>' . esc_html__('Web push engagement — campaign reach + response', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description" style="margin:0 0 14px;font-size:12px;color:#5b21b6;">'
            . esc_html__('Reads the active-subscriber count from wp_aieo_push_subscriptions and the response cohort from the attribution module (sessions tagged entry_channel = web_push). "Respond" = the customer clicked a push notification and landed on the site. Use the response rate to size campaigns: a 25% historical rate means a 10k push will earn ~2.5k clicks.', 'ai-eshop-optimizer')
            . '</p>';

        echo '<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:14px;">';
        $tiles = array(
            array(
                'label' => __('Active subscribers', 'ai-eshop-optimizer'),
                'value' => number_format_i18n($total_subs),
                'sub'   => sprintf(
                    
                    __('%1$s known + %2$s anonymous', 'ai-eshop-optimizer'),
                    number_format_i18n($known_subs),
                    number_format_i18n($anon_subs)
                ),
                'help'  => __('Push-notification subscriptions in `wp_aieo_push_subscriptions` with status=active. Logged-in users counted by user_id; anonymous devices counted as one subscriber per active endpoint (session_token-scoped). Anonymous subs get push but the response rate below uses only logged-in subs since responses are also tracked by user_id.', 'ai-eshop-optimizer'),
            ),
            array(
                'label' => __('Ever responded', 'ai-eshop-optimizer'),
                'value' => number_format_i18n($ever_responded),
                'sub'   => $response_rate . '% ' . __('of known subs', 'ai-eshop-optimizer'),
                'help'  => __('Distinct logged-in users who have at least one session tagged `entry_channel = web_push` in their entire history. Source: wp_aieo_sessions_statics + the attribution module. The percentage is the historical click-through rate per subscriber — multiply by your next push audience size to estimate clicks.', 'ai-eshop-optimizer'),
            ),
            array(
                'label' => __('Responded last 30d', 'ai-eshop-optimizer'),
                'value' => number_format_i18n($responded_30d),
                'sub'   => $recent_rate . '% ' . __('of known subs', 'ai-eshop-optimizer'),
                'help'  => __('Subscribers who clicked a push in the last 30 days. The freshest reachability signal — use as the high-confidence cohort for time-sensitive campaigns (flash sales, low-stock alerts).', 'ai-eshop-optimizer'),
            ),
            array(
                'label' => __('Response rate', 'ai-eshop-optimizer'),
                'value' => $response_rate . '%',
                'sub'   => __('lifetime, per subscriber', 'ai-eshop-optimizer'),
                'help'  => __('The headline planning number: of all logged-in subscribers, what fraction has ever clicked at least one push. Forecasting rule of thumb: campaign clicks ≈ audience × response_rate × (recency factor). Use the 30-day cohort for time-sensitive campaigns.', 'ai-eshop-optimizer'),
            ),
        );
        foreach ($tiles as $t) {
            $help = isset($t['help']) ? (string) $t['help'] : '';
            echo '<div class="aieo-attr-tile" style="background:#ede9fe;padding:10px;border-radius:6px;position:relative;">';
            echo '<div class="aieo-attr-tile-label" style="font-size:11px;color:#5b21b6;">';
            echo '<span>' . esc_html($t['label']) . '</span>';
            if ($help !== '') {
                echo '<span class="aieo-attr-tile-help" style="background:#ddd6fe;color:#5b21b6;" aria-label="' . esc_attr($help) . '">i</span>';
            }
            echo '</div>';
            echo '<div style="font-size:22px;font-weight:600;color:#4c1d95;margin:4px 0 2px;">' . esc_html($t['value']) . '</div>';
            echo '<div style="font-size:11px;color:#6d28d9;">' . esc_html($t['sub']) . '</div>';
            if ($help !== '') {
                echo '<div class="aieo-attr-tile-popover">' . esc_html($help) . '</div>';
            }
            echo '</div>';
        }
        echo '</div>';      
        echo '</div>';      
    }

    
    private function render_taste_vectors_card() {
        global $wpdb;
        $tbl_ci  = $wpdb->prefix . 'aieo_customer_intelligence';
        $tbl_emb = $wpdb->prefix . 'aieo_chat_embeddings';
        if ((string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl_ci)) !== $tbl_ci) { return; }

        $total       = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_ci}");
        $with_vector = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_ci} WHERE taste_vector IS NOT NULL");
        $stale       = (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM {$tbl_ci}
             WHERE (top_sole_product_id > 0 OR top_first_product_id > 0 OR top_last_product_id > 0)
               AND (taste_vector IS NULL
                    OR (last_order_at IS NOT NULL AND last_order_at > taste_vector_updated_at))"
        );
        $vector_bytes = (int) $wpdb->get_var("SELECT AVG(LENGTH(taste_vector)) FROM {$tbl_ci} WHERE taste_vector IS NOT NULL");
        $cov_pct = round(100 * $with_vector / max(1, $total), 1);

        $emb_total = (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl_emb)) === $tbl_emb
            ? (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_emb} WHERE LENGTH(embedding) > 100")
            : 0;
        $engine = get_option('aieo_chat_embedding_model', 'voyage-2');

        echo '<div class="aieo-dmm-card" style="background:#f5f3ff;border-left:3px solid #7c3aed;">';
        echo '<h3>' . esc_html__('Tier 3 — Voyage taste vectors', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description" style="margin:0 0 14px;font-size:12px;color:#5b21b6;">'
            . sprintf(
                
                esc_html__('Per-customer averaged embedding over their top sole / first / last products. Used in the k-NN distance metric as a third cosine term (weighted 4×). Currently using engine: %s.', 'ai-eshop-optimizer'),
                '<code>' . esc_html($engine) . '</code>'
            )
            . '</p>';

        echo '<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:14px;">';
        $tiles = array(
            array(
                'label' => __('Customers vectorised', 'ai-eshop-optimizer'),
                'value' => number_format_i18n($with_vector),
                'sub'   => $cov_pct . '% coverage',
                'help'  => __('Customers with a per-customer averaged taste-vector blob. Built from their top sole / first / last products via the Voyage embedding cache. Drives the embedding-cosine term in k-NN.', 'ai-eshop-optimizer'),
            ),
            array(
                'label' => __('Stale / pending', 'ai-eshop-optimizer'),
                'value' => number_format_i18n($stale),
                'sub'   => __('need (re)compute', 'ai-eshop-optimizer'),
                'help'  => __('Customers who have signature products but no taste vector yet, OR whose last_order_at moved forward since vectorisation. Hourly cron processes 50 per tick.', 'ai-eshop-optimizer'),
            ),
            array(
                'label' => __('Avg vector size', 'ai-eshop-optimizer'),
                'value' => number_format_i18n($vector_bytes) . ' B',
                'sub'   => $vector_bytes > 0 ? sprintf('~%d dims', (int) ($vector_bytes / 4)) : '—',
                'help'  => __('Average size in bytes of the stored taste-vector blob. Each dim is a float32 (4 bytes), so 6,144 B ≈ 1,536 dimensions — matches the Voyage-3-large model.', 'ai-eshop-optimizer'),
            ),
            array(
                'label' => __('Product embeddings', 'ai-eshop-optimizer'),
                'value' => number_format_i18n($emb_total),
                'sub'   => __('available in cache', 'ai-eshop-optimizer'),
                'help'  => __('Total product embeddings in the AIEO Chat embedding cache. Coverage of customer taste-vectors caps at "products in cache" — adding more product embeddings unblocks more customer vectorisation.', 'ai-eshop-optimizer'),
            ),
        );
        foreach ($tiles as $t) {
            echo '<div style="background:#ede9fe;padding:10px;border-radius:6px;position:relative;" class="aieo-attr-tile">';
            echo '<div style="font-size:11px;color:#5b21b6;text-transform:uppercase;letter-spacing:.05em;display:flex;align-items:center;gap:6px;">';
            echo '<span>' . esc_html($t['label']) . '</span>';
            echo '<span class="aieo-attr-tile-help" aria-label="' . esc_attr($t['help']) . '" style="background:#c4b5fd;color:#4c1d95;">i</span>';
            echo '</div>';
            echo '<div style="font-size:22px;font-weight:600;color:#4c1d95;margin:4px 0 2px;">' . esc_html($t['value']) . '</div>';
            echo '<div style="font-size:11px;color:#7c3aed;">' . esc_html($t['sub']) . '</div>';
            echo '<div class="aieo-attr-tile-popover">' . esc_html($t['help']) . '</div>';
            echo '</div>';
        }
        echo '</div>';

        echo '<p style="margin:8px 0 0;font-size:12px;color:#374151;">';
        echo esc_html__('Backfill runs automatically as part of "Recompute now" (full pass) and hourly via cron (50 stale customers per tick). Coverage rises as the product embedding cache fills.', 'ai-eshop-optimizer');
        echo '</p>';
        echo '</div>';
    }

    
    private function verify_price_historical_status() {
        global $wpdb;
        $tbl_orders   = $wpdb->prefix . 'aieo_temp_orders';
        $tbl_oi       = $wpdb->prefix . 'woocommerce_order_items';
        $tbl_oim      = $wpdb->prefix . 'woocommerce_order_itemmeta';

        
        if ((string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl_orders)) !== $tbl_orders) {
            return array('ok' => false, 'error' => 'wp_aieo_temp_orders not present', 'rows' => array());
        }
        if ((string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl_oi)) !== $tbl_oi) {
            return array('ok' => false, 'error' => 'wp_woocommerce_order_items not present (HPOS-only install?)', 'rows' => array());
        }

        
        
        
        $rows = $wpdb->get_results(
            "SELECT
                o.OrderId, o.OrderDate, o.SKU, o.ProductId, o.PrimaryQty,
                o.Price        AS aieo_price,
                o.RegularPrice AS aieo_regular,
                o.CurrentPrice AS aieo_current,
                o.OnSale       AS aieo_onsale,
                sub.wc_line_subtotal,
                sub.wc_line_total,
                sub.wc_qty
             FROM {$tbl_orders} o
             LEFT JOIN (
                 SELECT
                     oi.order_id,
                     CAST(MAX(IF(oim_pid.meta_key='_product_id',  oim_pid.meta_value,  NULL)) AS UNSIGNED) AS product_id,
                     MAX(IF(oim.meta_key='_line_subtotal', oim.meta_value, NULL)) AS wc_line_subtotal,
                     MAX(IF(oim.meta_key='_line_total',    oim.meta_value, NULL)) AS wc_line_total,
                     MAX(IF(oim.meta_key='_qty',           oim.meta_value, NULL)) AS wc_qty
                 FROM {$tbl_oi} oi
                 JOIN {$tbl_oim} oim     ON oim.order_item_id     = oi.order_item_id
                 JOIN {$tbl_oim} oim_pid ON oim_pid.order_item_id = oi.order_item_id
                                       AND oim_pid.meta_key       = '_product_id'
                 WHERE oi.order_item_type = 'line_item'
                 GROUP BY oi.order_item_id
             ) sub ON sub.order_id = o.OrderId AND sub.product_id = o.ProductId
             WHERE o.OrderDate IS NOT NULL
               AND o.OrderDate < DATE_SUB(UTC_TIMESTAMP(), INTERVAL 6 MONTH)
               AND o.Price IS NOT NULL
               AND o.PrimaryQty > 0
               AND sub.wc_line_subtotal IS NOT NULL
             ORDER BY o.OrderDate ASC
             LIMIT 10",
            ARRAY_A
        );

        return array('ok' => true, 'rows' => (array) $rows);
    }

    
    private function render_priority_diagnostics_card() {
        global $wpdb;
        $tbl_ci = $wpdb->prefix . 'aieo_customer_intelligence';
        $ci_exists = (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $tbl_ci)) === $tbl_ci;
        if (!$ci_exists) { return; }

        $total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_ci}");
        if ($total === 0) { return; }

        
        $row = $wpdb->get_row("SELECT
            -- discount_share buckets
            SUM(IF(IFNULL(discount_share,0) = 0, 1, 0))                             AS ds_0,
            SUM(IF(discount_share > 0    AND discount_share < 0.10, 1, 0))          AS ds_low,
            SUM(IF(discount_share >= 0.10 AND discount_share < 0.30, 1, 0))         AS ds_mid,
            SUM(IF(discount_share >= 0.30 AND discount_share < 0.50, 1, 0))         AS ds_high,
            SUM(IF(discount_share >= 0.50, 1, 0))                                   AS ds_max,
            -- price_band quintiles
            SUM(IF(price_band = 1, 1, 0)) AS pb_1,
            SUM(IF(price_band = 2, 1, 0)) AS pb_2,
            SUM(IF(price_band = 3, 1, 0)) AS pb_3,
            SUM(IF(price_band = 4, 1, 0)) AS pb_4,
            SUM(IF(price_band = 5, 1, 0)) AS pb_5,
            SUM(IF(price_band = 0 OR price_band IS NULL, 1, 0)) AS pb_unset,
            -- orders_count buckets
            SUM(IF(orders_count = 1, 1, 0))                  AS oc_1,
            SUM(IF(orders_count = 2, 1, 0))                  AS oc_2,
            SUM(IF(orders_count BETWEEN 3 AND 5, 1, 0))      AS oc_3_5,
            SUM(IF(orders_count BETWEEN 6 AND 10, 1, 0))     AS oc_6_10,
            SUM(IF(orders_count >= 11, 1, 0))                AS oc_11p,
            -- top_brand_share buckets
            SUM(IF(IFNULL(top_brand_share,0) < 0.30, 1, 0))                          AS tb_low,
            SUM(IF(top_brand_share >= 0.30 AND top_brand_share < 0.50, 1, 0))        AS tb_mid,
            SUM(IF(top_brand_share >= 0.50 AND top_brand_share < 0.70, 1, 0))        AS tb_high,
            SUM(IF(top_brand_share >= 0.70, 1, 0))                                   AS tb_max
            FROM {$tbl_ci}", ARRAY_A);

        
        $seg_rows = $wpdb->get_col("SELECT segments_json FROM {$tbl_ci} WHERE segments_json IS NOT NULL AND segments_json <> ''");
        $seg_counts = array();
        foreach ($seg_rows as $json) {
            $segs = json_decode((string) $json, true);
            if (!is_array($segs)) { continue; }
            foreach ($segs as $s) {
                $key = (string) $s;
                if (!isset($seg_counts[$key])) { $seg_counts[$key] = 0; }
                $seg_counts[$key]++;
            }
        }
        arsort($seg_counts);

        echo '<div class="aieo-dmm-card" style="background:#fffbeb;border-left:3px solid #d97706;">';
        echo '<h3>' . esc_html__('Heuristic diagnostics — CI signal distributions', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description" style="margin:0 0 14px;font-size:12px;color:#78350f;">'
            . sprintf(
                
                esc_html__('Across %s customers in customer_intelligence. Use this to tune the heuristic thresholds — if a rule like "price_band >= 4" matches 78%% of the base, it is too lenient and dominates the distribution. The point of v3 is to set thresholds where they actually carve up the population, not where they accept the majority.', 'ai-eshop-optimizer'),
                number_format_i18n($total) 
            )
            . '</p>'; 

        echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:18px;">';

        
        echo '<div>';
        echo '<h4 style="margin:0 0 6px;font-size:13px;">' . esc_html__('discount_share', 'ai-eshop-optimizer') . '</h4>';
        $this->mini_dist_bar(array(
            '0 (no coupon usage)'   => (int) $row['ds_0'],
            '0.01–0.09'             => (int) $row['ds_low'],
            '0.10–0.29'             => (int) $row['ds_mid'],
            '0.30–0.49'             => (int) $row['ds_high'],
            '0.50+ (heavy)'         => (int) $row['ds_max'],
        ), $total, '#dc2626');
        echo '<p style="margin:6px 0 0;font-size:11px;color:#9ca3af;">'
            . esc_html__('k-NN feature: discount_share × 2.0 (weighted to amplify the price-sensitivity axis). 3.8%+ heavy users are now a discoverable audience.', 'ai-eshop-optimizer')
            . '</p>';
        echo '</div>';

        
        echo '<div>';
        echo '<h4 style="margin:0 0 6px;font-size:13px;">' . esc_html__('price_band (quintile of avg item price)', 'ai-eshop-optimizer') . '</h4>';
        $this->mini_dist_bar(array(
            '1 (cheapest)' => (int) $row['pb_1'],
            '2'            => (int) $row['pb_2'],
            '3'            => (int) $row['pb_3'],
            '4'            => (int) $row['pb_4'],
            '5 (priciest)' => (int) $row['pb_5'],
            'unset'        => (int) $row['pb_unset'],
        ), $total, '#0d9488');

        
        
        
        
        $band_ranges = $wpdb->get_results(
            "SELECT price_band,
                    MIN(avg_item_price) AS min_p,
                    AVG(avg_item_price) AS avg_p,
                    MAX(avg_item_price) AS max_p
             FROM {$tbl_ci}
             WHERE price_band BETWEEN 1 AND 5 AND avg_item_price > 0
             GROUP BY price_band
             ORDER BY price_band ASC",
            ARRAY_A
        );
        if (!empty($band_ranges)) {
            echo '<table style="width:100%;font-size:11px;margin-top:6px;border-collapse:collapse;">';
            echo '<thead><tr style="background:#f9fafb;">'
                . '<th style="text-align:left;padding:3px 6px;">band</th>'
                . '<th style="text-align:right;padding:3px 6px;">min €</th>'
                . '<th style="text-align:right;padding:3px 6px;">avg €</th>'
                . '<th style="text-align:right;padding:3px 6px;">max €</th>'
                . '<th style="text-align:right;padding:3px 6px;">spread</th>'
                . '</tr></thead><tbody>';
            $overall_min = null; $overall_max = null;
            foreach ($band_ranges as $b) {
                $min = (float) $b['min_p'];
                $max = (float) $b['max_p'];
                $spread = $max - $min;
                if ($overall_min === null || $min < $overall_min) { $overall_min = $min; }
                if ($overall_max === null || $max > $overall_max) { $overall_max = $max; }
                echo '<tr>';
                echo '<td style="padding:3px 6px;">' . (int) $b['price_band'] . '</td>';
                echo '<td style="text-align:right;padding:3px 6px;">' . number_format_i18n($min, 2) . '</td>'; 
                echo '<td style="text-align:right;padding:3px 6px;color:#0d9488;font-weight:600;">' . number_format_i18n((float) $b['avg_p'], 2) . '</td>'; 
                echo '<td style="text-align:right;padding:3px 6px;">' . number_format_i18n($max, 2) . '</td>'; 
                echo '<td style="text-align:right;padding:3px 6px;color:#6b7280;">' . number_format_i18n($spread, 2) . '</td>'; 
                echo '</tr>'; 
            } 
            echo '</tbody></table>';

            
            
            $b1_max = (float) $band_ranges[0]['max_p'];
            $b5_max = (float) end($band_ranges)['max_p'];
            $ratio  = $b1_max > 0 ? round($b5_max / $b1_max, 1) : 0;
            $verdict_color = $ratio >= 5 ? '#15803d' : ($ratio >= 2 ? '#d97706' : '#b32d2e');
            $verdict_text  = $ratio >= 5
                ? __('Strong signal — wide price range across the base.', 'ai-eshop-optimizer')
                : ($ratio >= 2
                    ? __('Modest signal — bands meaningful but compressed.', 'ai-eshop-optimizer')
                    : __('Weak signal — bands are slicing a narrow price range. NTILE produces equal buckets regardless of true diversity; consider absolute price brackets instead.', 'ai-eshop-optimizer'));
            echo '<p style="margin:6px 0 0;font-size:11px;color:' . esc_attr($verdict_color) . ';">'
                . sprintf(
                    
                    esc_html__('Band-5 max / Band-1 max ratio: %1$s× — %2$s', 'ai-eshop-optimizer'),
                    $ratio, 
                    $verdict_text 
                ) 
                . '</p>'; 
        }
        echo '</div>';

        
        echo '<div>';
        echo '<h4 style="margin:0 0 6px;font-size:13px;">' . esc_html__('orders_count', 'ai-eshop-optimizer') . '</h4>';
        $this->mini_dist_bar(array(
            '1'    => (int) $row['oc_1'],
            '2'    => (int) $row['oc_2'],
            '3–5'  => (int) $row['oc_3_5'],
            '6–10' => (int) $row['oc_6_10'],
            '11+'  => (int) $row['oc_11p'],
        ), $total, '#7c3aed');
        echo '<p style="margin:6px 0 0;font-size:11px;color:#9ca3af;">'
            . esc_html__('"orders_count <= 2" was a major over-fire signal in v1. v2 only counts when paired with Newbie/Browser segment.', 'ai-eshop-optimizer')
            . '</p>';
        echo '</div>';

        
        echo '<div>';
        echo '<h4 style="margin:0 0 6px;font-size:13px;">' . esc_html__('top_brand_share', 'ai-eshop-optimizer') . '</h4>';
        $this->mini_dist_bar(array(
            '< 0.30 (diversified)' => (int) $row['tb_low'],
            '0.30–0.49'            => (int) $row['tb_mid'],
            '0.50–0.69'            => (int) $row['tb_high'],
            '0.70+ (concentrated)' => (int) $row['tb_max'],
        ), $total, '#0891b2');
        echo '<p style="margin:6px 0 0;font-size:11px;color:#9ca3af;">'
            . esc_html__('Quality uses > 0.5; store_trust uses > 0.7. Tightening either threshold redistributes mass.', 'ai-eshop-optimizer')
            . '</p>';
        echo '</div>';

        echo '</div>'; 

        
        if (!empty($seg_counts)) {
            echo '<h4 style="margin:18px 0 6px;font-size:13px;">' . esc_html__('Segment membership (segments_json)', 'ai-eshop-optimizer') . '</h4>';
            echo '<table style="width:100%;font-size:12px;">';
            $max_seg = max($seg_counts);
            foreach ($seg_counts as $seg => $cnt) {
                $pct = round(100 * $cnt / max(1, $total), 1);
                $w   = round(100 * $cnt / max(1, $max_seg), 1);
                echo '<tr>';
                echo '<td style="width:30%;color:#374151;"><strong>' . esc_html($seg) . '</strong></td>';
                echo '<td><div style="background:#f3f4f6;border-radius:3px;overflow:hidden;height:14px;"><div style="height:14px;background:#15803d;width:' . esc_attr($w) . '%;"></div></div></td>';
                echo '<td style="width:120px;text-align:right;color:#1f2937;">' . esc_html(number_format_i18n($cnt)) . ' (' . $pct . '%)</td>'; 
                echo '</tr>';
            } 
            echo '</table>';
            echo '<p style="margin:6px 0 0;font-size:11px;color:#9ca3af;">'
                . esc_html__('Segments drive several rules: Discount Hunter → price; Newbie/Browser → know_products; Brand-Devoted/Loyalist → store_trust. If any of these have ~0 customers, the rule never fires; if they have 50%+, the rule dominates.', 'ai-eshop-optimizer')
                . '</p>';
        }

        
        
        
        
        
        $ds_populated = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tbl_ci} WHERE discount_share > 0 OR avg_item_price > 0");
        $ds_label = $ds_populated > 0
            ? __('Refresh: recompute discount_share + price_band from WC line items', 'ai-eshop-optimizer')
            : __('Fix: compute discount_share + price_band from WC line items', 'ai-eshop-optimizer');
        $ds_caption = $ds_populated > 0
            ? sprintf(
                
                esc_html__('Already populated for %d customers via the recompute pipeline. Click to manually refresh — useful when new orders land between scheduled runs.', 'ai-eshop-optimizer'),
                $ds_populated
            )
            : esc_html__('discount_share is currently 100% zero and price_band is 100% unset because the AIEO data-prep pipeline did not populate temp_orders.Price/OnSale/RegularPrice. Click below to compute both directly from wp_woocommerce_order_itemmeta (authoritative): a coupon-applied line is detected as _line_total < _line_subtotal; per-customer avg unit price is _line_subtotal/_qty; price_band is NTILE(5) over the population of avg unit prices. HPOS-compatible.', 'ai-eshop-optimizer');

        echo '<h4 style="margin:18px 0 6px;font-size:13px;">' . esc_html($ds_label) . '</h4>';
        echo '<p style="margin:0 0 8px;font-size:12px;color:#374151;">' . $ds_caption . '</p>'; 
        echo '<p style="margin:0 0 6px;">';
        echo '<button type="button" class="button" id="aieo-surveys-fix-prices">' 
            . esc_html__('Recompute from WC line items', 'ai-eshop-optimizer')
            . '</button>';
        echo ' <span id="aieo-surveys-fix-prices-status" style="margin-left:10px;color:#646970;font-size:13px;"></span>';
        echo '</p>';
        echo $this->fix_prices_inline_js(); 

        
        
        
        
        echo '<h4 style="margin:18px 0 6px;font-size:13px;">' . esc_html__('Price columns: historical vs current?', 'ai-eshop-optimizer') . '</h4>';
        echo '<p style="margin:0 0 8px;font-size:12px;color:#374151;">'
            . esc_html__('Critical for the discount_share computation. Click below to compare temp_orders.Price against WC\'s _line_subtotal/_qty for 10 aged orders. If they match, columns are historical (we can compute discount_share correctly). If temp_orders.Price tracks the current product price instead, the columns are useless for discount-share inference and we\'d need to read directly from WC order item meta.', 'ai-eshop-optimizer')
            . '</p>';
        echo '<p style="margin:0 0 6px;">';
        echo '<button type="button" class="button" id="aieo-surveys-verify-prices">'
            . esc_html__('Run price-history verification', 'ai-eshop-optimizer')
            . '</button>';
        echo ' <span id="aieo-surveys-verify-prices-status" style="margin-left:10px;color:#646970;font-size:13px;"></span>';
        echo '</p>';
        echo '<div id="aieo-surveys-verify-prices-result" style="margin-top:10px;"></div>';
        echo $this->verify_prices_inline_js(); 

        echo '</div>'; 
    }

    private function fix_prices_inline_js() {
        $nonce = wp_create_nonce(AIEO_DMM::NONCE_ACTION);
        $ajax  = admin_url('admin-ajax.php');
        ob_start();
        ?>
        <script>
        (function(){
            const btn = document.getElementById('aieo-surveys-fix-prices');
            const status = document.getElementById('aieo-surveys-fix-prices-status');
            if (!btn) return;
            btn.addEventListener('click', async function(){
                btn.disabled = true;
                status.style.color = '#646970';
                status.textContent = 'Computing — joining WC line items, this may take 30-60s…';
                try {
                    const fd = new FormData();
                    fd.set('action', 'aieo_dmm_surveys_fix_prices');
                    fd.set('nonce', '<?php echo esc_js($nonce); ?>');
                    const resp = await fetch('<?php echo esc_js($ajax); ?>', { method:'POST', body:fd, credentials:'same-origin' });
                    const text = await resp.text();
                    let data;
                    try { data = JSON.parse(text); }
                    catch(_) {
                        status.style.color = '#b32d2e';
                        status.textContent = 'Server response not JSON: ' + text.slice(0, 200);
                        btn.disabled = false;
                        return;
                    }
                    if (data && data.success) {
                        status.style.color = '#15803d';
                        status.textContent = data.data.message;
                        setTimeout(() => window.location.reload(), 2000);
                    } else {
                        status.style.color = '#b32d2e';
                        status.textContent = (data && data.data && data.data.message) || 'Failed.';
                        btn.disabled = false;
                    }
                } catch (e) {
                    status.style.color = '#b32d2e';
                    status.textContent = String(e);
                    btn.disabled = false;
                }
            });
        })();
        </script>
        <?php
        return ob_get_clean();
    }

    private function verify_prices_inline_js() {
        $nonce = wp_create_nonce(AIEO_DMM::NONCE_ACTION);
        $ajax  = admin_url('admin-ajax.php');
        ob_start();
        ?>
        <script>
        (function(){
            const btn    = document.getElementById('aieo-surveys-verify-prices');
            const status = document.getElementById('aieo-surveys-verify-prices-status');
            const out    = document.getElementById('aieo-surveys-verify-prices-result');
            if (!btn) return;

            function fmt(v) { return v == null ? '—' : String(v); }

            btn.addEventListener('click', async function(){
                btn.disabled = true;
                status.style.color = '#646970';
                status.textContent = 'Querying…';
                out.innerHTML = '';
                try {
                    const fd = new FormData();
                    fd.set('action', 'aieo_dmm_surveys_verify_prices');
                    fd.set('nonce', '<?php echo esc_js($nonce); ?>');
                    const resp = await fetch('<?php echo esc_js($ajax); ?>', { method:'POST', body:fd, credentials:'same-origin' });
                    const data = await resp.json();
                    if (!data || !data.success) {
                        status.style.color = '#b32d2e';
                        status.textContent = (data && data.data && data.data.message) || 'Failed.';
                        btn.disabled = false;
                        return;
                    }
                    const rows = (data.data && data.data.rows) || [];
                    if (rows.length === 0) {
                        status.style.color = '#78350f';
                        status.textContent = 'No matching aged orders found (need orders > 6 months old with WC item meta).';
                        btn.disabled = false;
                        return;
                    }

                    // Decide verdict per row: aieo_price ≈ wc_line_subtotal/wc_qty → historical match.
                    let historical_matches = 0, current_matches = 0, ambiguous = 0;
                    rows.forEach(r => {
                        const aieoPrice = parseFloat(r.aieo_price);
                        const aieoCurrent = parseFloat(r.aieo_current);
                        const wcSubtotal = parseFloat(r.wc_line_subtotal);
                        const wcQty = parseFloat(r.wc_qty) || 1;
                        const wcUnit = wcSubtotal / wcQty;
                        if (Math.abs(aieoPrice - wcUnit) < 0.05) historical_matches++;
                        else if (Math.abs(aieoPrice - aieoCurrent) < 0.05) current_matches++;
                        else ambiguous++;
                    });

                    let verdictColor = '#646970', verdictText = '';
                    if (historical_matches >= rows.length * 0.7) {
                        verdictColor = '#15803d';
                        verdictText = 'HISTORICAL — temp_orders.Price matches WC line price at order time. Safe to compute discount_share from these columns.';
                    } else if (current_matches >= rows.length * 0.7) {
                        verdictColor = '#b32d2e';
                        verdictText = 'CURRENT — temp_orders.Price tracks current product state. Cannot compute historical discount_share from these columns; need to read WC order item meta directly.';
                    } else {
                        verdictColor = '#d97706';
                        verdictText = 'MIXED / INCONCLUSIVE — some rows match historical, some match current, some neither. Worth a deeper dive before relying on these columns.';
                    }

                    let html = '<div style="padding:10px 14px;margin-bottom:10px;border-left:3px solid ' + verdictColor + ';background:#f9fafb;font-size:13px;color:' + verdictColor + ';"><strong>Verdict: ' + verdictText + '</strong><br><span style="color:#374151;font-size:12px;">historical_matches=' + historical_matches + ', current_matches=' + current_matches + ', ambiguous=' + ambiguous + ' / total=' + rows.length + '</span></div>';

                    html += '<table class="widefat striped" style="font-size:11px;">';
                    html += '<thead><tr>';
                    html += '<th>OrderId</th><th>OrderDate</th><th>SKU</th><th>Qty</th>';
                    html += '<th>aieo Price</th><th>aieo Regular</th><th>aieo Current</th><th>OnSale</th>';
                    html += '<th>WC subtotal</th><th>WC unit</th><th>match</th>';
                    html += '</tr></thead><tbody>';
                    rows.forEach(r => {
                        const aieoPrice = parseFloat(r.aieo_price);
                        const aieoCurrent = parseFloat(r.aieo_current);
                        const wcSubtotal = parseFloat(r.wc_line_subtotal);
                        const wcQty = parseFloat(r.wc_qty) || 1;
                        const wcUnit = wcSubtotal / wcQty;
                        let label = '?', color = '#9ca3af';
                        if (Math.abs(aieoPrice - wcUnit) < 0.05) { label = 'historical'; color = '#15803d'; }
                        else if (Math.abs(aieoPrice - aieoCurrent) < 0.05) { label = 'current'; color = '#b32d2e'; }
                        else label = 'neither';
                        html += '<tr>';
                        html += '<td>' + fmt(r.OrderId) + '</td>';
                        html += '<td>' + fmt(r.OrderDate) + '</td>';
                        html += '<td><code>' + fmt(r.SKU) + '</code></td>';
                        html += '<td>' + fmt(r.PrimaryQty) + '</td>';
                        html += '<td>' + fmt(r.aieo_price) + '</td>';
                        html += '<td>' + fmt(r.aieo_regular) + '</td>';
                        html += '<td>' + fmt(r.aieo_current) + '</td>';
                        html += '<td>' + fmt(r.aieo_onsale) + '</td>';
                        html += '<td>' + fmt(r.wc_line_subtotal) + '</td>';
                        html += '<td>' + (isFinite(wcUnit) ? wcUnit.toFixed(2) : '—') + '</td>';
                        html += '<td style="color:' + color + ';font-weight:600;">' + label + '</td>';
                        html += '</tr>';
                    });
                    html += '</tbody></table>';

                    out.innerHTML = html;
                    status.style.color = '#15803d';
                    status.textContent = 'Done.';
                    btn.disabled = false;
                } catch (e) {
                    status.style.color = '#b32d2e';
                    status.textContent = String(e);
                    btn.disabled = false;
                }
            });
        })();
        </script>
        <?php
        return ob_get_clean();
    }

    
    private function mini_dist_bar(array $bins, $total, $color) {
        if (empty($bins)) { return; }
        $max = max($bins) ?: 1;
        echo '<table style="width:100%;font-size:12px;">';
        foreach ($bins as $label => $cnt) {
            $pct = round(100 * $cnt / max(1, $total), 1);
            $w   = round(100 * $cnt / $max, 1);
            echo '<tr>';
            echo '<td style="width:35%;color:#374151;">' . esc_html($label) . '</td>';
            echo '<td><div style="background:#f3f4f6;border-radius:3px;overflow:hidden;height:13px;"><div style="height:13px;background:' . esc_attr($color) . ';width:' . esc_attr($w) . '%;"></div></div></td>';
            echo '<td style="width:90px;text-align:right;color:#1f2937;">' . esc_html(number_format_i18n($cnt)) . ' (' . $pct . '%)</td>'; 
            echo '</tr>';
        } 
        echo '</table>';
    }

    
    private function build_label_to_slug_map(array $grank_to_label) {
        $out = array();
        foreach ($grank_to_label as $g => $label) {
            $low = mb_strtolower($label);
            if (strpos($low, 'τιμές') !== false || strpos($low, 'τιμη') !== false) {
                $out[$label] = 'price';
            } elseif (strpos($low, 'κόστος αποστολ') !== false || strpos($low, 'αποστολ') !== false) {
                $out[$label] = 'shipping_cost';
            } elseif (strpos($low, 'ποιότητα προϊ') !== false || strpos($low, 'ποιοτητα προι') !== false) {
                $out[$label] = 'quality';
            } elseif (strpos($low, 'ιστοσελ') !== false || strpos($low, 'ταχύτητα - ποιότητα') !== false) {
                $out[$label] = 'site_quality';
            } elseif (strpos($low, 'ταχύτητα εξυπηρ') !== false) {
                $out[$label] = 'service_speed';
            } elseif (strpos($low, 'υποστήριξ') !== false) {
                $out[$label] = 'support';
            } elseif (strpos($low, 'αξιοπιστ') !== false) {
                $out[$label] = 'store_trust';
            } elseif (strpos($low, 'φυσικ') !== false || strpos($low, 'φυσιό') !== false) {
                $out[$label] = 'physical_store';
            } elseif (strpos($low, 'ξέρω τα προϊ') !== false || strpos($low, 'ξερω τα προι') !== false) {
                $out[$label] = 'know_products';
            }
        }
        return $out;
    }

    
    private function render_loyalty_journey_card($tbl_si) {
        global $wpdb;
        $tbl_sr = $wpdb->prefix . 'aieo_survey_responses';

        
        $r = $wpdb->get_row("SELECT
            COUNT(*) total,
            SUM(CASE WHEN orders_before_survey > 1 THEN 1 ELSE 0 END) returning_at_survey,
            SUM(CASE WHEN orders_before_survey <= 1 THEN 1 ELSE 0 END) firsttime_at_survey,
            SUM(CASE WHEN orders_after_survey > 0 THEN 1 ELSE 0 END) bought_again,
            SUM(CASE WHEN orders_after_survey = 0 THEN 1 ELSE 0 END) silent_after,
            SUM(spend_before_survey) total_spend_before,
            SUM(spend_after_survey)  total_spend_after,
            SUM(CASE WHEN orders_before_survey > 0 OR orders_after_survey > 0 THEN 1 ELSE 0 END) any_orders,
            SUM(orders_before_survey) total_orders_before,
            SUM(orders_after_survey)  total_orders_after,
            SUM(spend_before_survey) / NULLIF(SUM(orders_before_survey), 0) aov_before,
            SUM(spend_after_survey)  / NULLIF(SUM(orders_after_survey),  0) aov_after
            FROM {$tbl_si} WHERE f18_entry_id > 0", ARRAY_A);

        $total = (int) ($r['total'] ?? 0);

        echo '<div class="aieo-dmm-card">';
        echo '<h3>' . esc_html__('Survey-respondent loyalty journey', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description" style="margin:0 0 10px;font-size:12px;">'
            . esc_html__('Cross-references the F18 responder set against AIEO order history. The first row answers "who were they before the survey?", the second "what did they do after?". The bottom 2×2 splits returners by whether they also filled the survey a second time — the gold cohort gives us explicit drift, the silent cohort gives us implicit feedback through repurchase.', 'ai-eshop-optimizer')
            . '</p>';

        if ($total === 0) {
            echo '<p style="margin:0;padding:10px 14px;background:#fffbeb;border-left:3px solid #d97706;font-size:13px;color:#78350f;">'
                . esc_html__('No data yet — press "Recompute now" above to build wp_aieo_survey_intelligence from the ingested responses. Recompute also runs the purchase-correlation step that joins your AIEO order snapshots to fill orders_before / orders_after / spend_before / spend_after.', 'ai-eshop-optimizer')
                . '</p>';
            echo '</div>';
            return;
        }

        
        
        
        
        $any_orders = (int) ($r['any_orders'] ?? 0);
        if ($any_orders === 0) {
            echo '<p style="margin:0 0 12px;padding:10px 14px;background:#fef2f2;border-left:3px solid #b32d2e;font-size:13px;color:#7f1d1d;">'
                . esc_html__('Survey intelligence has rows but the purchase-correlation columns are all zero. compute_purchase_metrics() likely failed to find any wp_aieo_*_orders snapshots, or none of the responder eponymous_ids matched. Re-run Recompute and check debug.log if it persists.', 'ai-eshop-optimizer')
                . '</p>';
        }

        
        
        $cohort = $wpdb->get_row("SELECT
            SUM(CASE WHEN si.orders_after_survey > 0 AND rep.cnt > 1  THEN 1 ELSE 0 END) bought_refilled,
            SUM(CASE WHEN si.orders_after_survey > 0 AND (rep.cnt IS NULL OR rep.cnt = 1) THEN 1 ELSE 0 END) bought_silent,
            SUM(CASE WHEN si.orders_after_survey = 0 AND rep.cnt > 1 THEN 1 ELSE 0 END) norebuy_refilled,
            SUM(CASE WHEN si.orders_after_survey = 0 AND (rep.cnt IS NULL OR rep.cnt = 1) THEN 1 ELSE 0 END) norebuy_silent
            FROM {$tbl_si} si
            LEFT JOIN (
                SELECT eponymous_id, COUNT(*) cnt
                FROM {$tbl_sr} WHERE form_id = 18 AND eponymous_id > 0
                GROUP BY eponymous_id
            ) rep ON rep.eponymous_id = si.eponymous_id
            WHERE si.f18_entry_id > 0", ARRAY_A);

        $returning   = (int) $r['returning_at_survey'];
        $firsttime   = (int) $r['firsttime_at_survey'];
        $bought      = (int) $r['bought_again'];
        $silent      = (int) $r['silent_after'];
        $aov_before  = (float) $r['aov_before'];
        $aov_after   = (float) $r['aov_after'];
        $aov_lift    = $aov_before > 0 ? round(100 * ($aov_after - $aov_before) / $aov_before, 1) : 0;

        echo '<h4 style="margin:14px 0 6px;font-size:13px;">' . esc_html__('At time of survey', 'ai-eshop-optimizer') . '</h4>';
        echo '<div class="aieo-attr-tiles">';
        $this->tile(
            __('Returning customers', 'ai-eshop-optimizer'),
            number_format_i18n($returning) . ' (' . round(100 * $returning / max(1, $total), 1) . '%)',
            __('Of all F18 responders, how many had already placed 2+ orders BEFORE submitting the survey. These were already loyal customers when they answered.', 'ai-eshop-optimizer')
        );
        $this->tile(
            __('First-time at survey', 'ai-eshop-optimizer'),
            number_format_i18n($firsttime) . ' (' . round(100 * $firsttime / max(1, $total), 1) . '%)',
            __('Of all F18 responders, how many had 0 or 1 prior orders when submitting. These were brand-new (or barely past their first order) when they answered.', 'ai-eshop-optimizer')
        );
        $this->tile(
            __('Avg AOV before', 'ai-eshop-optimizer'),
            number_format_i18n($aov_before, 2) . ' €',
            __('Average order value across every order any F18 responder placed BEFORE their own f18_submitted_at. Calculated as total revenue ÷ total orders (industry-standard AOV).', 'ai-eshop-optimizer')
        );
        echo '</div>';

        echo '<h4 style="margin:18px 0 6px;font-size:13px;">' . esc_html__('After the survey', 'ai-eshop-optimizer') . '</h4>';
        echo '<div class="aieo-attr-tiles">';
        $this->tile(
            __('Bought again', 'ai-eshop-optimizer'),
            number_format_i18n($bought) . ' (' . round(100 * $bought / max(1, $total), 1) . '%)',
            __('Of all F18 responders, how many placed at least one order AFTER submitting. The implicit-positive cohort — they returned without leaving a second survey.', 'ai-eshop-optimizer')
        );
        $this->tile(
            __('Silent (no return)', 'ai-eshop-optimizer'),
            number_format_i18n($silent) . ' (' . round(100 * $silent / max(1, $total), 1) . '%)',
            __('Of all F18 responders, how many placed zero orders AFTER submitting. The win-back target audience — they answered the survey and went quiet.', 'ai-eshop-optimizer')
        );
        $this->tile(
            __('Avg AOV after', 'ai-eshop-optimizer'),
            number_format_i18n($aov_after, 2) . ' €',
            __('Average order value across every order placed AFTER each responder\'s f18_submitted_at. Same total-revenue ÷ total-orders formula as Avg AOV before.', 'ai-eshop-optimizer')
        );
        $this->tile(
            __('AOV lift', 'ai-eshop-optimizer'),
            ($aov_lift >= 0 ? '+' : '') . number_format_i18n($aov_lift, 1) . '%',
            __('Percentage change between Avg AOV after and Avg AOV before. Positive = customers spend more per order after taking the survey; negative = they spend less.', 'ai-eshop-optimizer')
        );
        echo '</div>';

        
        $br = (int) ($cohort['bought_refilled']  ?? 0);
        $bs = (int) ($cohort['bought_silent']    ?? 0);
        $nr = (int) ($cohort['norebuy_refilled'] ?? 0);
        $ns = (int) ($cohort['norebuy_silent']   ?? 0);

        echo '<h4 style="margin:18px 0 6px;font-size:13px;">' . esc_html__('Returner × refiller cohorts', 'ai-eshop-optimizer') . '</h4>';
        echo '<table class="widefat striped" style="max-width:680px;">';
        echo '<thead><tr>';
        echo '<th></th>';
        echo '<th>' . esc_html__('Refilled F18', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('No second survey', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('Row total', 'ai-eshop-optimizer') . '</th>';
        echo '</tr></thead><tbody>';
        echo '<tr><th>' . esc_html__('Bought again', 'ai-eshop-optimizer') . '</th>';
        echo '<td><strong>' . number_format_i18n($br) . '</strong> <small style="color:#15803d;">' . esc_html__('explicit drift cohort', 'ai-eshop-optimizer') . '</small></td>'; 
        echo '<td><strong>' . number_format_i18n($bs) . '</strong> <small style="color:#0891b2;">' . esc_html__('implicit positive', 'ai-eshop-optimizer') . '</small></td>'; 
        echo '<td>' . number_format_i18n($br + $bs) . '</td></tr>'; 
        echo '<tr><th>' . esc_html__('Did not buy again', 'ai-eshop-optimizer') . '</th>'; 
        echo '<td>' . number_format_i18n($nr) . '</td>'; 
        echo '<td><strong>' . number_format_i18n($ns) . '</strong> <small style="color:#9ca3af;">' . esc_html__('lost / silent', 'ai-eshop-optimizer') . '</small></td>'; 
        echo '<td>' . number_format_i18n($nr + $ns) . '</td></tr>'; 
        echo '</tbody></table>'; 
        echo '</div>'; 
    }

    
    private function render_repeat_respondents_card($tbl_sr) {
        global $wpdb;

        
        
        $rows = $wpdb->get_results(
            "WITH ranked AS (
                SELECT eponymous_id, email, response_id, submitted_at,
                       f18_nps, f18_satisfaction, f18_site_ux_avg,
                       ROW_NUMBER() OVER (PARTITION BY eponymous_id ORDER BY submitted_at ASC,  response_id ASC)  rn_first,
                       ROW_NUMBER() OVER (PARTITION BY eponymous_id ORDER BY submitted_at DESC, response_id DESC) rn_last,
                       COUNT(*)    OVER (PARTITION BY eponymous_id) total_responses
                FROM {$tbl_sr}
                WHERE form_id = 18 AND eponymous_id > 0
            )
            SELECT
                f.eponymous_id, f.email, f.total_responses,
                f.submitted_at first_at, l.submitted_at last_at,
                DATEDIFF(l.submitted_at, f.submitted_at) days_between,
                f.f18_nps           nps_first,
                l.f18_nps           nps_last,
                f.f18_satisfaction  sat_first,
                l.f18_satisfaction  sat_last,
                f.f18_site_ux_avg   ux_first,
                l.f18_site_ux_avg   ux_last,
                (CAST(l.f18_nps AS SIGNED) - CAST(f.f18_nps AS SIGNED)) nps_delta,
                (CAST(l.f18_satisfaction AS SIGNED) - CAST(f.f18_satisfaction AS SIGNED)) sat_delta
            FROM ranked f
            JOIN ranked l ON l.eponymous_id = f.eponymous_id AND l.rn_last = 1
            WHERE f.rn_first = 1 AND f.total_responses > 1",
            ARRAY_A
        );

        $total_repeats = count($rows);

        echo '<div class="aieo-dmm-card">';
        echo '<h3>' . esc_html__('Sentiment drift on repeat F18', 'ai-eshop-optimizer') . '</h3>';
        echo '<p class="description" style="margin:0 0 10px;font-size:12px;">'
            . esc_html__('Customers who filled F18 after a second order — same questionnaire, two timepoints. Positive deltas mean they got happier, negative means churn-risk drift. The biggest swings are the most actionable: a -3 NPS swing on a high-LTV customer is rescue priority, a +2 swing is a referral-program seed.', 'ai-eshop-optimizer')
            . '</p>';

        if ($total_repeats === 0) {
            echo '<p style="margin:0;color:#9ca3af;font-size:13px;">'
                . esc_html__('No customers have filled F18 more than once yet — every responder is a single-shot. As repeat orders flow through, this cohort builds up automatically.', 'ai-eshop-optimizer')
                . '</p>';
            echo '</div>';
            return;
        }

        
        $more_positive = $more_negative = $unchanged = 0;
        $sum_nps_delta = 0; $sum_sat_delta = 0; $sum_days = 0;
        foreach ($rows as $r) {
            $combo = (int) $r['nps_delta'] + (int) $r['sat_delta'];
            if ($combo > 0)      { $more_positive++; }
            elseif ($combo < 0)  { $more_negative++; }
            else                 { $unchanged++; }
            $sum_nps_delta += (int) $r['nps_delta'];
            $sum_sat_delta += (int) $r['sat_delta'];
            $sum_days      += (int) $r['days_between'];
        }
        $avg_nps_delta = round($sum_nps_delta / $total_repeats, 2);
        $avg_sat_delta = round($sum_sat_delta / $total_repeats, 2);
        $avg_days      = round($sum_days / $total_repeats, 0);

        echo '<div class="aieo-attr-tiles">';
        $this->tile(__('Repeat F18 responders', 'ai-eshop-optimizer'), number_format_i18n($total_repeats),
            __('Customers who filled F18 at least twice — same questionnaire, two timepoints. Their first→second swing is the cleanest read on whether the experience improved or degraded.', 'ai-eshop-optimizer'));
        $this->tile(__('More positive', 'ai-eshop-optimizer'),
            number_format_i18n($more_positive) . ' (' . round(100 * $more_positive / max(1, $total_repeats), 1) . '%)',
            __('Repeat responders whose combined NPS+Satisfaction delta is above zero — they got happier between the two surveys. Promote candidates for advocate programs.', 'ai-eshop-optimizer'));
        $this->tile(__('More negative', 'ai-eshop-optimizer'),
            number_format_i18n($more_negative) . ' (' . round(100 * $more_negative / max(1, $total_repeats), 1) . '%)',
            __('Repeat responders whose combined delta is below zero — drift toward churn. The biggest swings on high-LTV customers are the priority rescue list.', 'ai-eshop-optimizer'));
        $this->tile(__('Unchanged', 'ai-eshop-optimizer'),
            number_format_i18n($unchanged) . ' (' . round(100 * $unchanged / max(1, $total_repeats), 1) . '%)',
            __('Repeat responders whose combined delta = 0 — sentiment held steady. Largest cell in most healthy shops; gives a baseline against which positive/negative deltas are meaningful.', 'ai-eshop-optimizer'));
        $this->tile(__('Avg NPS Δ', 'ai-eshop-optimizer'),
            ($avg_nps_delta >= 0 ? '+' : '') . number_format_i18n($avg_nps_delta, 2),
            __('Mean change in NPS score from first F18 to last F18 across all repeat responders. Positive = relationship deepening, negative = trust eroding.', 'ai-eshop-optimizer'));
        $this->tile(__('Avg Sat. Δ', 'ai-eshop-optimizer'),
            ($avg_sat_delta >= 0 ? '+' : '') . number_format_i18n($avg_sat_delta, 2),
            __('Mean change in Overall-Satisfaction score from first to last F18. Tracked separately from NPS because someone can stay loyal (high NPS) while quietly satisfaction-decaying.', 'ai-eshop-optimizer'));
        $this->tile(__('Avg gap', 'ai-eshop-optimizer'),
            number_format_i18n($avg_days) . ' ' . esc_html__('days', 'ai-eshop-optimizer'),
            __('Average days between a repeat responder\'s first F18 submission and their most recent. Gives the natural sentiment-revisit cadence for these customers.', 'ai-eshop-optimizer'));
        echo '</div>';

        
        usort($rows, function($a, $b) {
            $ab = abs((int) $a['nps_delta'] + (int) $a['sat_delta']);
            $bb = abs((int) $b['nps_delta'] + (int) $b['sat_delta']);
            return $bb <=> $ab;
        });
        $top = array_slice($rows, 0, 20);

        echo '<h4 style="margin:18px 0 6px;font-size:13px;">' . esc_html__('Biggest swings (first → last)', 'ai-eshop-optimizer') . '</h4>';
        echo '<table class="widefat striped">';
        echo '<thead><tr>';
        echo '<th>' . esc_html__('Email', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('Responses', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('Days apart', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('NPS', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('Δ NPS', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('Sat.', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('Δ Sat.', 'ai-eshop-optimizer') . '</th>';
        echo '</tr></thead><tbody>';
        foreach ($top as $r) {
            $nps_delta = (int) $r['nps_delta'];
            $sat_delta = (int) $r['sat_delta'];
            $delta_color = ($nps_delta + $sat_delta) > 0 ? '#15803d' : (($nps_delta + $sat_delta) < 0 ? '#b32d2e' : '#646970');
            echo '<tr>';
            echo '<td><strong>' . esc_html((string) $r['email']) . '</strong></td>';
            echo '<td>' . (int) $r['total_responses'] . '</td>';
            echo '<td>' . (int) $r['days_between'] . '</td>';
            echo '<td>' . (int) $r['nps_first'] . ' → ' . (int) $r['nps_last'] . '</td>';
            echo '<td style="color:' . esc_attr($delta_color) . ';"><strong>' . ($nps_delta >= 0 ? '+' : '') . $nps_delta . '</strong></td>'; 
            echo '<td>' . (int) $r['sat_first'] . ' → ' . (int) $r['sat_last'] . '</td>';
            echo '<td style="color:' . esc_attr($delta_color) . ';"><strong>' . ($sat_delta >= 0 ? '+' : '') . $sat_delta . '</strong></td>'; 
            echo '</tr>';
        } 
        echo '</tbody></table>';
        echo '</div>';
    }

    private function render_phase5_todo_card() {
        echo '<div class="aieo-dmm-card" style="background:#f0f9ff;border-left:3px solid #0891b2;">';
        echo '<h3 style="margin:0 0 6px;">' . esc_html__('Form 20 dispatch trigger (Phase 5+ TODO)', 'ai-eshop-optimizer') . '</h3>';
        echo '<p style="margin:0;font-size:13px;color:#0c4a6e;">';
        echo esc_html__('Form 20 (post-delivery satisfaction) is structurally ready — fields cover delivery speed, packaging, per-product expectation match, repurchase intent, gift category preference, and personal traits. We have 11 entries currently. The plan is to email the form a week after dispatch via WC order-status hooks, prioritising customers who answered "Ναι" on F18 q70 (1,400+ pre-consented). When that trigger lands in Phase 5+, the personal-profile data + gift_category_preference unlock visual product matching and gift-occasion campaigns we can\'t do today.', 'ai-eshop-optimizer');
        echo '</p>';
        echo '</div>';
    }

    
    
    

    
    private function render_competition_signal_card() {
        if (!class_exists('AIEO_Competition_Analytics') && defined('AIEO_DIR')) {
            foreach (array('class-aieo-competition-cpt.php', 'class-aieo-competition-participants.php', 'class-aieo-competition-analytics.php') as $f) {
                $p = AIEO_DIR . 'includes/dmm/competitions/' . $f;
                if (file_exists($p)) { require_once $p; }
            }
        }
        if (!class_exists('AIEO_Competition_Analytics')) { return; }

        $agg = AIEO_Competition_Analytics::aggregate(10);
        if ((int) $agg['total_participants'] === 0) { return; } 

        $cur = function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol() : '';
        $pct = $agg['unique_emails'] > 0 ? round(100 * $agg['identified'] / $agg['unique_emails'], 1) : 0;

        echo '<div class="aieo-dmm-card">';
        echo '<h3>' . esc_html__('🎉 Competition engagement', 'ai-eshop-optimizer') . '</h3>';
        echo '<p style="margin:0 0 10px;font-size:13px;color:#475569;">'
            . esc_html__('Prize-competition entry as a customer signal — how your giveaway entrants overlap with known customers and which are worth most. Rolled up across all competitions (per-competition detail lives on each competition).', 'ai-eshop-optimizer')
            . '</p>';

        echo '<div class="aieo-attr-tiles">';
        $this->tile(__('Unique participants', 'ai-eshop-optimizer'), number_format_i18n($agg['unique_emails']),
            __('Distinct people (by email) who entered any competition.', 'ai-eshop-optimizer'));
        $this->tile(__('Known customers among them', 'ai-eshop-optimizer'), number_format_i18n($agg['identified']) . ' (' . $pct . '%)',
            __('Entrants resolved to a customer in the CDP (eponymous_id) — the overlap between competition entrants and your customer base.', 'ai-eshop-optimizer'));
        if (!empty($agg['has_order_data'])) {
            $this->tile(__('Entrants who ordered', 'ai-eshop-optimizer'), number_format_i18n($agg['converting_entrants']),
                __('Entrants whose email matches at least one order — competition entry → purchase.', 'ai-eshop-optimizer'));
            $this->tile(__('Revenue from entrants', 'ai-eshop-optimizer'), $cur . number_format_i18n($agg['entrant_revenue'], 2),
                __('Total lifetime order revenue from people who entered a competition.', 'ai-eshop-optimizer'));
        }
        echo '</div>';

        if (!empty($agg['top_participants'])) {
            echo '<h4 style="margin:14px 0 6px;">' . esc_html__('Top competition participants by lifetime value', 'ai-eshop-optimizer') . '</h4>';
            echo '<table class="widefat striped" style="max-width:760px;"><thead><tr>';
            echo '<th>' . esc_html__('Customer', 'ai-eshop-optimizer') . '</th><th>' . esc_html__('Competitions entered', 'ai-eshop-optimizer') . '</th><th>' . esc_html__('Wins', 'ai-eshop-optimizer') . '</th><th>' . esc_html__('Lifetime value', 'ai-eshop-optimizer') . '</th>';
            echo '</tr></thead><tbody>';
            foreach ($agg['top_participants'] as $tp) {
                echo '<tr><td>' . esc_html((string) $tp->email) . '</td><td>' . (int) $tp->entries . '</td><td>' . (int) $tp->wins . '</td><td>' . esc_html($cur . number_format_i18n((float) $tp->ltv_total, 2)) . '</td></tr>';
            }
            echo '</tbody></table>';
        }
        echo '</div>';
    }

    private function tile($label, $value, $help = '') {
        echo '<div class="aieo-attr-tile">';
        echo '<div class="aieo-attr-tile-label">';
        echo '<span>' . esc_html($label) . '</span>';
        if ($help !== '') {
            echo '<span class="aieo-attr-tile-help" aria-label="' . esc_attr($help) . '">i</span>';
        }
        echo '</div>';
        echo '<div class="aieo-attr-tile-value">' . esc_html((string) $value) . '</div>';
        if ($help !== '') {
            echo '<div class="aieo-attr-tile-popover">' . esc_html($help) . '</div>';
        }
        echo '</div>';
    }

    private function mini_bar_chart($rows, $color) {
        if (empty($rows)) {
            echo '<p style="color:#9ca3af;font-size:12px;">' . esc_html__('No data yet.', 'ai-eshop-optimizer') . '</p>';
            return;
        }
        $max = 0;
        foreach ($rows as $r) { $max = max($max, (int) $r['c']); }
        echo '<table style="width:100%;font-size:12px;">';
        foreach ($rows as $r) {
            $w = $max > 0 ? round(100 * (int) $r['c'] / $max, 1) : 0;
            echo '<tr>';
            echo '<td style="width:30%;color:#374151;">' . esc_html((string) $r['v']) . '</td>';
            echo '<td><div style="background:#f3f4f6;border-radius:3px;overflow:hidden;height:14px;">';
            echo '<div style="height:14px;background:' . esc_attr($color) . ';width:' . esc_attr((string) $w) . '%;"></div>';
            echo '</div></td>';
            echo '<td style="width:60px;text-align:right;color:#1f2937;">' . esc_html(number_format_i18n((int) $r['c'])) . '</td>';
            echo '</tr>';
        }
        echo '</table>';
    }

    private function actionable_table($rows, $show_feedback) {
        if (empty($rows)) {
            echo '<p style="margin:0;color:#9ca3af;font-size:12px;">' . esc_html__('No matching customers (yet — recompute first).', 'ai-eshop-optimizer') . '</p>';
            return;
        }
        echo '<table class="widefat striped">';
        echo '<thead><tr>';
        echo '<th>' . esc_html__('Email', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('LTV', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('NPS', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('Sat.', 'ai-eshop-optimizer') . '</th>';
        echo '<th>' . esc_html__('VoC', 'ai-eshop-optimizer') . '</th>';
        if ($show_feedback) { echo '<th>' . esc_html__('Open feedback', 'ai-eshop-optimizer') . '</th>'; }
        echo '</tr></thead><tbody>';
        foreach ($rows as $r) {
            echo '<tr>';
            echo '<td><strong>' . esc_html((string) $r['email']) . '</strong></td>';
            echo '<td>' . esc_html(number_format_i18n((float) $r['ltv_total'], 2)) . ' €</td>';
            echo '<td>' . (int) $r['nps_score'] . '</td>';
            echo '<td>' . (int) $r['overall_satisfaction'] . '</td>';
            echo '<td>' . (int) $r['voc_score'] . '</td>';
            if ($show_feedback) {
                $fb = (string) ($r['open_feedback_f18'] ?? '');
                echo '<td><small style="color:#374151;">' . esc_html(mb_substr($fb, 0, 140)) . (mb_strlen($fb) > 140 ? '…' : '') . '</small></td>';
            }
            echo '</tr>';
        }
        echo '</tbody></table>';
    }

    
    private function async_buttons_js() {
        $nonce = wp_create_nonce(AIEO_DMM::NONCE_ACTION);
        $ajax  = admin_url('admin-ajax.php');
        $kick  = esc_js(__('Working in background…', 'ai-eshop-optimizer'));
        ob_start();
        ?>
        <script>
        (function(){
            const NONCE = '<?php echo esc_js($nonce); ?>';
            const AJAX  = '<?php echo esc_js($ajax); ?>';
            const POLL_MS = 2000;
            const POLL_MAX = 600; // 600 × 2s = 20 minutes max

            function bind(btnId, statusId, action, expectedKind) {
                const btn = document.getElementById(btnId);
                const status = document.getElementById(statusId);
                if (!btn) return;
                btn.addEventListener('click', async function(){
                    btn.disabled = true;
                    status.style.color = '#646970';
                    status.textContent = '<?php echo esc_js($kick); ?>';
                    try {
                        const fd = new FormData();
                        fd.set('action', action);
                        fd.set('nonce', NONCE);
                        const resp = await fetch(AJAX, { method:'POST', body:fd, credentials:'same-origin' });
                        const text = await resp.text();
                        let data;
                        try { data = JSON.parse(text); }
                        catch(_) {
                            status.style.color = '#b32d2e';
                            status.textContent = 'Server response not JSON: ' + text.slice(0, 120);
                            btn.disabled = false;
                            return;
                        }
                        if (!data || !data.success) {
                            status.style.color = '#b32d2e';
                            status.textContent = (data && data.data && data.data.message) || 'Failed to queue.';
                            btn.disabled = false;
                            return;
                        }
                        // Queued — start polling, but only for OUR kind.
                        pollUntilDone(status, btn, expectedKind);
                    } catch (e) {
                        status.style.color = '#b32d2e';
                        status.textContent = String(e);
                        btn.disabled = false;
                    }
                });
            }

            async function pollUntilDone(status, btn, expectedKind) {
                let n = 0;
                const interval = setInterval(async () => {
                    n++;
                    if (n > POLL_MAX) {
                        clearInterval(interval);
                        status.style.color = '#b32d2e';
                        status.textContent = 'Timed out after 20 minutes — check debug.log.';
                        btn.disabled = false;
                        return;
                    }
                    try {
                        const fd = new FormData();
                        fd.set('action', 'aieo_dmm_surveys_status');
                        fd.set('nonce', NONCE);
                        const resp = await fetch(AJAX, { method:'POST', body:fd, credentials:'same-origin' });
                        const data = await resp.json();
                        if (!data || !data.success) return;
                        const s = data.data;
                        // Bail if the global state belongs to a different operation —
                        // the user pressed the other button. Stop polling, re-enable
                        // our button, and let the other poller run.
                        if (s.kind && s.kind !== expectedKind) {
                            clearInterval(interval);
                            status.style.color = '#9ca3af';
                            status.textContent = '';
                            btn.disabled = false;
                            return;
                        }
                        if (s.state === 'running' || s.state === 'queued') {
                            const total_elapsed = Date.now()/1000 - (s.started || 0);
                            const mins = Math.floor(total_elapsed / 60);
                            const secs = Math.floor(total_elapsed % 60);
                            // Render rich per-stage progress when the
                            // server publishes it (recompute pipeline);
                            // fall back to plain elapsed otherwise.
                            const p = s.progress || null;
                            if (p && p.current && p.total) {
                                const pct = Math.round(100 * (p.current - 1) / p.total);
                                const stage_elapsed = Math.floor(Date.now()/1000 - (p.started || 0));

                                // Sub-progress (e.g. priority inference
                                // writes processed / total every 30s so
                                // we can show "12,438 / 33,071" while a
                                // long stage grinds.
                                let subHTML = '';
                                const sp = s.subprogress || null;
                                if (sp && sp.total) {
                                    const subPct = Math.min(100, Math.round(100 * (sp.processed || 0) / sp.total));
                                    const elapsed = sp.elapsed || 0;
                                    const rate = elapsed > 0 ? (sp.processed / elapsed) : 0;
                                    const remaining = rate > 0 ? Math.round((sp.total - sp.processed) / rate) : 0;
                                    const etaMin = Math.floor(remaining / 60);
                                    const etaSec = remaining % 60;
                                    subHTML =
                                        '<div style="margin-top:8px;font-size:12px;color:#374151;">' +
                                          '<span style="font-weight:500;">' + (sp.label || 'Processed') + ':</span> ' +
                                          sp.processed.toLocaleString() + ' / ' + sp.total.toLocaleString() +
                                          ' (' + subPct + '%)' +
                                          (rate > 0 ? ' · ETA ~' + etaMin + 'm ' + etaSec + 's' : '') +
                                        '</div>' +
                                        '<div style="background:#f3f4f6;border-radius:3px;overflow:hidden;height:4px;margin-top:4px;width:420px;max-width:100%;">' +
                                          '<div style="background:#a78bfa;height:4px;width:' + subPct + '%;transition:width .3s;"></div>' +
                                        '</div>';
                                }

                                status.innerHTML =
                                    '<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">' +
                                      '<span style="font-weight:600;color:#1d4ed8;">Stage ' + p.current + '/' + p.total + '</span>' +
                                      '<span>' + (p.label ? p.label.replace(/[<>]/g, '') : '...') + '</span>' +
                                      '<span style="color:#9ca3af;">· ' + stage_elapsed + 's stage · ' + mins + 'm ' + secs + 's total</span>' +
                                    '</div>' +
                                    '<div style="background:#e5e7eb;border-radius:3px;overflow:hidden;height:6px;margin-top:6px;width:420px;max-width:100%;">' +
                                      '<div style="background:#1d4ed8;height:6px;width:' + pct + '%;transition:width .3s;"></div>' +
                                    '</div>' +
                                    subHTML;
                            } else {
                                status.textContent = (s.state === 'queued' ? 'Queued' : 'Running') +
                                    ' · ' + mins + 'm ' + secs + 's elapsed…';
                            }
                            return;
                        }
                        if (s.state === 'done') {
                            clearInterval(interval);
                            status.style.color = '#15803d';
                            status.textContent = s.message || 'Done.';
                            setTimeout(() => { window.location.reload(); }, 1500);
                        } else if (s.state === 'error') {
                            clearInterval(interval);
                            status.style.color = '#b32d2e';
                            status.textContent = 'Failed: ' + (s.message || 'unknown error');
                            btn.disabled = false;
                        }
                    } catch (e) { /* swallow transient poll errors */ }
                }, POLL_MS);
            }

            bind('aieo-surveys-ingest',    'aieo-surveys-ingest-status',    'aieo_dmm_surveys_ingest',    'ingest');
            bind('aieo-surveys-recompute', 'aieo-surveys-recompute-status', 'aieo_dmm_surveys_recompute', 'recompute');
        })();
        </script>
        <?php
        return ob_get_clean();
    }

    private function recompute_inline_js() { return $this->async_buttons_js(); }
    private function ingest_inline_js()    { return '';  }
}
