<?php

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

if ( ! class_exists( 'AIEO_Ratings', false ) ) {

class AIEO_Ratings {

    
    const COOKIE_NAME = 'aieo_rating_session';
    const COOKIE_DAYS = 180;

    
    const SCHEMA_VERSION = '2';
    const SCHEMA_OPTION  = 'aieo_ratings_schema_version';

    
    const SETTINGS_OPTION = 'aieo_ratings_settings';

    
    const CACHE_GROUP  = 'aieo_ratings';
    const CACHE_PREFIX = 'stats_';

    
    const NONCE_ACTION = 'aieo_ratings';

    
    private static $instance = null;

    public static function instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        
        
        add_action('wp_ajax_aieo_rating_save',        array($this, 'ajax_save'));
        add_action('wp_ajax_nopriv_aieo_rating_save', array($this, 'ajax_save'));
        add_action('wp_ajax_aieo_rating_get',         array($this, 'ajax_get'));
        add_action('wp_ajax_nopriv_aieo_rating_get',  array($this, 'ajax_get'));

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

        
        add_action('plugins_loaded', array($this, 'maybe_install'), 20);

        
        add_shortcode('aieo_rating_stars',     array($this, 'shortcode_stars'));
        add_shortcode('aieo_rating_aggregate', array($this, 'shortcode_aggregate'));

        
        
        
        
        
        $s = self::get_settings();
        if (!empty($s['enabled'])) {
            if (!empty($s['display_archive'])) {
                add_action('woocommerce_after_shop_loop_item', array($this, 'render_loop_widget'), 12);
                
                
                
                add_action('loop_start', array($this, 'prime_loop_ratings'));
            }
            if (!empty($s['display_single'])) {
                $hook = 'woocommerce_single_product_summary';
                $pri  = 6;
                add_action($hook, array($this, 'render_single_widget'), $pri);
            }
        }
        
        
    }

    
    
    

    public static function events_table() {
        global $wpdb;
        return $wpdb->prefix . 'aieo_ratings';
    }

    public static function stats_table() {
        global $wpdb;
        return $wpdb->prefix . 'aieo_rating_stats';
    }

    
    public function maybe_install() {
        if (get_option(self::SCHEMA_OPTION, '') === self::SCHEMA_VERSION) {
            return;
        }
        self::install();
        update_option(self::SCHEMA_OPTION, self::SCHEMA_VERSION, false);
    }

    public static function install() {
        global $wpdb;
        $events  = self::events_table();
        $stats   = self::stats_table();
        $charset = $wpdb->get_charset_collate();

        
        
        
        
        $sql_events = "CREATE TABLE {$events} (
            id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            object_type varchar(20) NOT NULL DEFAULT 'product',
            product_id bigint(20) UNSIGNED NOT NULL,
            user_id bigint(20) UNSIGNED NOT NULL DEFAULT 0,
            session_token varchar(64) NOT NULL DEFAULT '',
            rating tinyint(4) NOT NULL DEFAULT 0,
            previous_rating tinyint(4) NOT NULL DEFAULT 0,
            source varchar(32) NOT NULL DEFAULT '',
            user_agent_hash char(16) NOT NULL DEFAULT '',
            ip_hash char(16) NOT NULL DEFAULT '',
            created_at datetime NOT NULL,
            PRIMARY KEY  (id),
            KEY idx_product (object_type, product_id, created_at),
            KEY idx_user (user_id, created_at),
            KEY idx_session (session_token, created_at)
        ) {$charset};";

        $sql_stats = "CREATE TABLE {$stats} (
            object_type varchar(20) NOT NULL DEFAULT 'product',
            product_id bigint(20) UNSIGNED NOT NULL,
            count_total int(10) UNSIGNED NOT NULL DEFAULT 0,
            sum_total int(10) UNSIGNED NOT NULL DEFAULT 0,
            n1 int(10) UNSIGNED NOT NULL DEFAULT 0,
            n2 int(10) UNSIGNED NOT NULL DEFAULT 0,
            n3 int(10) UNSIGNED NOT NULL DEFAULT 0,
            n4 int(10) UNSIGNED NOT NULL DEFAULT 0,
            n5 int(10) UNSIGNED NOT NULL DEFAULT 0,
            last_voted_at datetime NULL,
            PRIMARY KEY  (object_type, product_id),
            KEY idx_count (count_total)
        ) {$charset};";

        if (!function_exists('dbDelta')) {
            require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        }
        dbDelta($sql_events);
        dbDelta($sql_stats);

        
        
        
        self::ensure_v2_object_type_columns();
    }

    
    private static function ensure_v2_object_type_columns() {
        global $wpdb;
        $events = self::events_table();
        $stats  = self::stats_table();

        $prev_suppress = isset($wpdb->suppress_errors) ? (bool) $wpdb->suppress_errors : false;
        $wpdb->suppress_errors(true);

        
        
        $col = $wpdb->get_var("SHOW COLUMNS FROM `{$events}` LIKE 'object_type'");
        if (!$col) {
            
            $wpdb->query("ALTER TABLE `{$events}` ADD COLUMN `object_type` VARCHAR(20) NOT NULL DEFAULT 'product' AFTER `id`");
        }
        
        
        
        
        $idx = $wpdb->get_results("SHOW INDEX FROM `{$events}` WHERE Key_name = 'idx_product'", ARRAY_A);
        $needs_idx_rebuild = false;
        if (is_array($idx) && count($idx) > 0) {
            $cols_in_idx = array_map(function ($r) { return strtolower((string) $r['Column_name']); }, $idx);
            if (!in_array('object_type', $cols_in_idx, true)) {
                $needs_idx_rebuild = true;
            }
        }
        if ($needs_idx_rebuild) {
            
            $wpdb->query("ALTER TABLE `{$events}` DROP INDEX `idx_product`, ADD KEY `idx_product` (`object_type`, `product_id`, `created_at`)");
        }

        
        
        $col = $wpdb->get_var("SHOW COLUMNS FROM `{$stats}` LIKE 'object_type'");
        if (!$col) {
            
            $wpdb->query("ALTER TABLE `{$stats}` ADD COLUMN `object_type` VARCHAR(20) NOT NULL DEFAULT 'product' FIRST");
            
            
            
            
            $wpdb->query("ALTER TABLE `{$stats}` DROP PRIMARY KEY, ADD PRIMARY KEY (`object_type`, `product_id`)");
        }

        $wpdb->suppress_errors($prev_suppress);
    }

    
    
    

    
    public static function get_settings() {
        $stored = get_option(self::SETTINGS_OPTION, array());
        if (!is_array($stored)) { $stored = array(); }
        return array_merge(array(
            'enabled'                 => 1,
            'require_login'           => 0,
            'login_redirect_mode'     => 'redirect',     
            'display_archive'         => 1,
            'display_single'          => 1,
            'archive_position'        => 'under_title',  
            'single_position'         => 'under_title',
            'aggregate_only_archive'  => 0,
            
            'caption_format'          => '%avg (%count)',
            
            'caption_before_rated'    => __('Give your vote to see the ratings', 'ai-eshop-optimizer'),
            'min_votes_for_display'   => 0,
            'display_full_until_rated' => 0,
            'color_empty'             => '#d4d4d8',
            
            'color_filled'            => '#F46767',
            'color_hover'             => '#F46767',
            
            
            'color_pre_rated'         => '#000000',
            'size'                    => 20,
            
            'size_mobile'             => 24,
            
            'size_single'             => 36,
            
            
            
            
            'gap'                     => 2,
            'gap_mobile'              => 1,
            
            
            
            'caption_size'                => 13,
            'caption_size_mobile'         => 13,
            'caption_size_archive'        => 12,
            'caption_size_archive_mobile' => 11,
            'allow_clear'             => 1,
            
            
            
            
            
            
            'seo_schema_enabled'      => 1,
        ), $stored);
    }

    
    
    

    
    private function get_or_create_session_token() {
        if (isset($_COOKIE[self::COOKIE_NAME]) && (string) $_COOKIE[self::COOKIE_NAME] !== '') {
            $raw = (string) wp_unslash($_COOKIE[self::COOKIE_NAME]);
            $token = preg_replace('/[^a-z0-9]/i', '', $raw);
            if ($token !== '') { return substr($token, 0, 64); }
        }
        $token = wp_generate_password(32, false, false);
        if (!headers_sent()) {
            setcookie(
                self::COOKIE_NAME,
                $token,
                time() + (self::COOKIE_DAYS * DAY_IN_SECONDS),
                COOKIEPATH ? COOKIEPATH : '/',
                COOKIE_DOMAIN
            );
        }
        $_COOKIE[self::COOKIE_NAME] = $token;
        return $token;
    }

    private function read_session_token_no_create() {
        if (!isset($_COOKIE[self::COOKIE_NAME])) { return ''; }
        $raw = (string) wp_unslash($_COOKIE[self::COOKIE_NAME]);
        $token = preg_replace('/[^a-z0-9]/i', '', $raw);
        return $token === '' ? '' : substr($token, 0, 64);
    }

    
    private static function short_hash($value) {
        return substr(md5((string) $value), 0, 16);
    }

    
    
    

    
    public function ajax_save() {
        check_ajax_referer(self::NONCE_ACTION, 'nonce');

        $object_type = isset($_POST['object_type']) ? sanitize_key((string) wp_unslash($_POST['object_type'])) : 'product';
        if (!in_array($object_type, array('product', 'brand'), true)) { $object_type = 'product'; }

        $pid    = isset($_POST['product_id']) ? absint(wp_unslash($_POST['product_id'])) : 0;
        $rating = isset($_POST['rating'])     ? (int) wp_unslash($_POST['rating']) : -1;
        $source = isset($_POST['source'])     ? sanitize_key((string) wp_unslash($_POST['source'])) : '';

        if ($pid <= 0) {
            wp_send_json_error(array('message' => __('Invalid id.', 'ai-eshop-optimizer')));
        }
        if ($rating < 0 || $rating > 5) {
            wp_send_json_error(array('message' => __('Invalid rating value.', 'ai-eshop-optimizer')));
        }

        if ($object_type === 'product') {
            $post_type = get_post_type($pid);
            if ($post_type !== 'product' && $post_type !== 'product_variation') {
                wp_send_json_error(array('message' => __('Not a product.', 'ai-eshop-optimizer')));
            }
        } elseif ($object_type === 'brand') {
            if (!term_exists($pid, 'product_brand')) {
                wp_send_json_error(array('message' => __('Invalid brand term ID.', 'ai-eshop-optimizer')));
            }
        }

        $settings = self::get_settings();
        $user_id  = get_current_user_id();
        if (empty($settings['enabled'])) {
            wp_send_json_error(array('message' => __('Ratings are disabled.', 'ai-eshop-optimizer')));
        }
        if (!empty($settings['require_login']) && $user_id <= 0) {
            wp_send_json_error(array(
                'message'        => __('Please sign in to rate.', 'ai-eshop-optimizer'),
                'login_required' => true,
                'login_url'      => wp_login_url(isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : ''),
            ));
        }

        $token = $this->get_or_create_session_token();

        $stats = $this->record_event($pid, $rating, $user_id, $token, $source, $object_type);

        wp_send_json_success(array_merge($stats, array(
            'current_rating' => $rating,
            'object_type'    => $object_type,
        )));
    }

    
    public function ajax_get() {
        check_ajax_referer(self::NONCE_ACTION, 'nonce');

        $object_type = isset($_REQUEST['object_type']) ? sanitize_key((string) wp_unslash($_REQUEST['object_type'])) : 'product';
        if (!in_array($object_type, array('product', 'brand'), true)) { $object_type = 'product'; }

        $pid = isset($_REQUEST['product_id']) ? absint(wp_unslash($_REQUEST['product_id'])) : 0;
        if ($pid <= 0) {
            wp_send_json_error(array('message' => __('Invalid id.', 'ai-eshop-optimizer')));
        }

        $stats   = $this->get_stats($pid, $object_type);
        $current = $this->get_current_rating_for_viewer($pid, get_current_user_id(), $this->read_session_token_no_create(), $object_type);

        wp_send_json_success(array_merge($stats, array(
            'current_rating' => $current,
            'object_type'    => $object_type,
        )));
    }

    
    
    

    
    public function record_event($product_id, $rating, $user_id, $session_token, $source = '', $object_type = 'product') {
        global $wpdb;
        $product_id = (int) $product_id;
        $rating     = max(0, min(5, (int) $rating));
        $user_id    = max(0, (int) $user_id);
        $token      = (string) $session_token;
        $object_type = in_array((string) $object_type, array('product', 'brand'), true) ? (string) $object_type : 'product';

        $previous = $this->get_current_rating_for_viewer($product_id, $user_id, $token, $object_type);

        $now    = current_time('mysql', true);
        $ip_raw = isset($_SERVER['REMOTE_ADDR'])     ? (string) wp_unslash($_SERVER['REMOTE_ADDR']) : '';
        $ua_raw = isset($_SERVER['HTTP_USER_AGENT']) ? (string) wp_unslash($_SERVER['HTTP_USER_AGENT']) : '';

        $wpdb->insert(
            self::events_table(),
            array(
                'object_type'     => $object_type,
                'product_id'      => $product_id,
                'user_id'         => $user_id,
                'session_token'   => $token,
                'rating'          => $rating,
                'previous_rating' => (int) $previous,
                'source'          => (string) $source,
                'user_agent_hash' => self::short_hash($ua_raw),
                'ip_hash'         => self::short_hash($ip_raw),
                'created_at'      => $now,
            ),
            array('%s','%d','%d','%s','%d','%d','%s','%s','%s','%s')
        );

        
        $count_delta = ($rating > 0 ? 1 : 0) - ($previous > 0 ? 1 : 0);
        $sum_delta   = $rating - (int) $previous;
        $bucket_decr = array_fill(1, 5, 0);
        if ($previous > 0) { $bucket_decr[(int) $previous] = -1; }
        if ($rating > 0)   { $bucket_decr[(int) $rating]  += 1; }

        $stats_table = self::stats_table();
        $seed_count = max(0, $count_delta);
        $seed_sum   = max(0, $sum_delta);
        $seed_n     = array(1 => max(0, $bucket_decr[1]), 2 => max(0, $bucket_decr[2]), 3 => max(0, $bucket_decr[3]), 4 => max(0, $bucket_decr[4]), 5 => max(0, $bucket_decr[5]));

        $sql = $wpdb->prepare(
            "INSERT INTO {$stats_table}
                (object_type, product_id, count_total, sum_total, n1, n2, n3, n4, n5, last_voted_at)
             VALUES
                (%s, %d, %d, %d, %d, %d, %d, %d, %d, %s)
             ON DUPLICATE KEY UPDATE
                count_total   = GREATEST(0, CAST(count_total AS SIGNED) + %d),
                sum_total     = GREATEST(0, CAST(sum_total   AS SIGNED) + %d),
                n1            = GREATEST(0, CAST(n1 AS SIGNED) + %d),
                n2            = GREATEST(0, CAST(n2 AS SIGNED) + %d),
                n3            = GREATEST(0, CAST(n3 AS SIGNED) + %d),
                n4            = GREATEST(0, CAST(n4 AS SIGNED) + %d),
                n5            = GREATEST(0, CAST(n5 AS SIGNED) + %d),
                last_voted_at = VALUES(last_voted_at)",
            $object_type,
            $product_id,
            $seed_count,
            $seed_sum,
            $seed_n[1], $seed_n[2], $seed_n[3], $seed_n[4], $seed_n[5],
            $now,
            $count_delta,
            $sum_delta,
            $bucket_decr[1], $bucket_decr[2], $bucket_decr[3], $bucket_decr[4], $bucket_decr[5]
        );
        $wpdb->query($sql);

        
        wp_cache_delete(self::CACHE_PREFIX . $object_type . ':' . $product_id, self::CACHE_GROUP);

        $fresh = $this->get_stats($product_id, $object_type);

        
        
        
        if ($object_type === 'product') {
            $this->write_rating_to_vitals($product_id, $fresh);
        }

        return $fresh;
    }

    
    private function write_rating_to_vitals($product_id, $stats) {
        global $wpdb;
        $product_id = (int) $product_id;
        if ($product_id <= 0) { return; }
        $vitals = $wpdb->prefix . 'aieo_core_product_vitals';

        static $has_cols = null;
        if ($has_cols === null) {
            $has_cols = ((int) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM information_schema.COLUMNS
                 WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = 'rating_value'",
                $vitals
            ))) > 0;
        }
        if (!$has_cols) { return; }

        $count = isset($stats['count']) ? (int) $stats['count'] : 0;
        $avg   = ($count > 0 && isset($stats['avg'])) ? round((float) $stats['avg'], 2) : null;

        if ($avg === null) {
            
            $wpdb->query($wpdb->prepare("UPDATE `{$vitals}` SET rating_value = NULL, rating_count = %d WHERE product_id = %d", $count, $product_id));
        } else {
            
            $wpdb->query($wpdb->prepare("UPDATE `{$vitals}` SET rating_value = %f, rating_count = %d WHERE product_id = %d", $avg, $count, $product_id));
        }
    }

    
    public static function sync_all_ratings_to_vitals() {
        global $wpdb;
        $vitals = $wpdb->prefix . 'aieo_core_product_vitals';
        $stats  = self::stats_table();

        $has_cols = ((int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM information_schema.COLUMNS
             WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = 'rating_value'",
            $vitals
        ))) > 0;
        if (!$has_cols) { return; }

        
        $wpdb->query(
            "UPDATE `{$vitals}` v
             JOIN (
                SELECT product_id,
                       ROUND(sum_total / count_total, 2) AS avg_val,
                       count_total
                FROM `{$stats}`
                WHERE object_type = 'product' AND count_total > 0
             ) s ON s.product_id = v.product_id
             SET v.rating_value = s.avg_val,
                 v.rating_count = s.count_total"
        );
    }

    
    private $viewer_rating_memo = array();

    
    private $viewer_memo_sig = null;

    
    public function prime_loop_ratings($query) {
        if (is_admin()) { return; }
        if (!($query instanceof WP_Query) || !$query->is_main_query()) { return; }
        if (!function_exists('is_shop')
            || !(is_shop() || is_product_taxonomy() || is_post_type_archive('product'))) {
            return;
        }
        $ids = array();
        foreach ((array) $query->posts as $p) {
            $pid = is_object($p) ? (int) $p->ID : (int) $p;
            if ($pid > 0) { $ids[] = $pid; }
        }
        if (empty($ids)) { return; }
        
        $this->get_stats_batch($ids);
        
        $this->prime_viewer_ratings($ids);
    }

    
    private function prime_viewer_ratings(array $ids, $object_type = 'product') {
        $object_type = in_array((string) $object_type, array('product', 'brand'), true) ? (string) $object_type : 'product';
        $user_id = (int) get_current_user_id();
        $token   = (string) $this->read_session_token_no_create();
        $this->viewer_memo_sig = $object_type . '|' . $user_id . '|' . $token;

        $ids = array_values(array_unique(array_filter(array_map('intval', $ids), function ($x) { return $x > 0; })));
        if (empty($ids)) { return; }

        
        if ($user_id <= 0 && $token === '') {
            foreach ($ids as $pid) { $this->viewer_rating_memo[$object_type . ':' . $pid] = 0; }
            return;
        }

        global $wpdb;
        $events = self::events_table();
        $ph = implode(',', array_fill(0, count($ids), '%d'));
        if ($user_id > 0) {
            $args  = array_merge(array($object_type), $ids, array($user_id));
            $scope = 'user_id = %d';
        } else {
            $args  = array_merge(array($object_type), $ids, array($token));
            $scope = 'user_id = 0 AND session_token = %s';
        }
        
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT product_id, rating FROM (
                SELECT product_id, rating,
                       ROW_NUMBER() OVER (PARTITION BY product_id ORDER BY created_at DESC, id DESC) AS rn
                FROM {$events}
                WHERE object_type = %s AND product_id IN ({$ph}) AND {$scope}
             ) t WHERE rn = 1",
            $args
        ), ARRAY_A);

        $found = array();
        foreach ((array) $rows as $r) { $found[(int) $r['product_id']] = (int) $r['rating']; }
        foreach ($ids as $pid) {
            $this->viewer_rating_memo[$object_type . ':' . $pid] = isset($found[$pid]) ? $found[$pid] : 0;
        }
    }

    
    public function get_current_rating_for_viewer($product_id, $user_id, $session_token, $object_type = 'product') {
        global $wpdb;
        $product_id = (int) $product_id;
        $user_id    = (int) $user_id;
        $token      = (string) $session_token;
        $object_type = in_array((string) $object_type, array('product', 'brand'), true) ? (string) $object_type : 'product';
        if ($product_id <= 0) { return 0; }

        
        
        
        $memo_sig = $object_type . '|' . $user_id . '|' . $token;
        $memo_key = $object_type . ':' . $product_id;
        if ($this->viewer_memo_sig === $memo_sig && array_key_exists($memo_key, $this->viewer_rating_memo)) {
            return (int) $this->viewer_rating_memo[$memo_key];
        }

        $events = self::events_table();
        if ($user_id > 0) {
            $row = $wpdb->get_row($wpdb->prepare(
                "SELECT rating FROM {$events}
                 WHERE object_type = %s AND product_id = %d AND user_id = %d
                 ORDER BY created_at DESC, id DESC LIMIT 1",
                $object_type, $product_id, $user_id
            ), ARRAY_A);
        } elseif ($token !== '') {
            $row = $wpdb->get_row($wpdb->prepare(
                "SELECT rating FROM {$events}
                 WHERE object_type = %s AND product_id = %d AND user_id = 0 AND session_token = %s
                 ORDER BY created_at DESC, id DESC LIMIT 1",
                $object_type, $product_id, $token
            ), ARRAY_A);
        } else {
            return 0;
        }
        if (!$row) { return 0; }
        return (int) $row['rating'];
    }

    
    
    

    
    public function get_stats($product_id, $object_type = 'product') {
        $product_id = (int) $product_id;
        $object_type = in_array((string) $object_type, array('product', 'brand'), true) ? (string) $object_type : 'product';
        if ($product_id <= 0) {
            return $this->empty_stats();
        }
        $cache_key = self::CACHE_PREFIX . $object_type . ':' . $product_id;
        $cached    = wp_cache_get($cache_key, self::CACHE_GROUP);
        if (is_array($cached)) { return $cached; }

        global $wpdb;
        $row = $wpdb->get_row($wpdb->prepare(
            "SELECT count_total, sum_total, n1, n2, n3, n4, n5, last_voted_at
             FROM " . self::stats_table() . "
             WHERE object_type = %s AND product_id = %d",
            $object_type, $product_id
        ), ARRAY_A);

        if (!$row) {
            $payload = $this->empty_stats();
        } else {
            $count = (int) $row['count_total'];
            $sum   = (int) $row['sum_total'];
            $avg   = $count > 0 ? round($sum / $count, 1) : 0.0;
            $payload = array(
                'count'         => $count,
                'sum'           => $sum,
                'avg'           => (float) $avg,
                'distribution'  => array(
                    (int) $row['n1'], (int) $row['n2'], (int) $row['n3'], (int) $row['n4'], (int) $row['n5'],
                ),
                'last_voted_at' => $row['last_voted_at'] ? (string) $row['last_voted_at'] : '',
            );
        }
        wp_cache_set($cache_key, $payload, self::CACHE_GROUP, 5 * MINUTE_IN_SECONDS);
        $payload['object_type'] = $object_type;
        return $payload;
    }

    
    public function get_stats_batch(array $ids, $object_type = 'product') {
        $object_type = in_array((string) $object_type, array('product', 'brand'), true) ? (string) $object_type : 'product';
        $ids = array_values(array_unique(array_filter(array_map('intval', $ids), function ($x) { return $x > 0; })));
        if (empty($ids)) { return array(); }

        $out = array();
        $miss = array();
        foreach ($ids as $pid) {
            $cached = wp_cache_get(self::CACHE_PREFIX . $object_type . ':' . $pid, self::CACHE_GROUP);
            if (is_array($cached)) { $out[$pid] = $cached; }
            else                    { $miss[]    = $pid; }
        }

        if (!empty($miss)) {
            global $wpdb;
            $placeholders = implode(',', array_fill(0, count($miss), '%d'));
            $args = array_merge(array($object_type), $miss);
            $rows = $wpdb->get_results($wpdb->prepare(
                "SELECT product_id, count_total, sum_total, n1, n2, n3, n4, n5, last_voted_at
                 FROM " . self::stats_table() . "
                 WHERE object_type = %s AND product_id IN ({$placeholders})",
                $args
            ), ARRAY_A);
            $by_pid = array();
            foreach ((array) $rows as $r) {
                $by_pid[(int) $r['product_id']] = $r;
            }
            foreach ($miss as $pid) {
                if (isset($by_pid[$pid])) {
                    $r     = $by_pid[$pid];
                    $count = (int) $r['count_total'];
                    $sum   = (int) $r['sum_total'];
                    $avg   = $count > 0 ? round($sum / $count, 1) : 0.0;
                    $out[$pid] = array(
                        'count'         => $count,
                        'sum'           => $sum,
                        'avg'           => (float) $avg,
                        'distribution'  => array(
                            (int) $r['n1'], (int) $r['n2'], (int) $r['n3'], (int) $r['n4'], (int) $r['n5'],
                        ),
                        'last_voted_at' => $r['last_voted_at'] ? (string) $r['last_voted_at'] : '',
                    );
                } else {
                    $out[$pid] = $this->empty_stats();
                }
                wp_cache_set(self::CACHE_PREFIX . $object_type . ':' . $pid, $out[$pid], self::CACHE_GROUP, 5 * MINUTE_IN_SECONDS);
            }
        }

        return $out;
    }

    private function empty_stats() {
        return array(
            'count'         => 0,
            'sum'           => 0,
            'avg'           => 0.0,
            'distribution'  => array(0, 0, 0, 0, 0),
            'last_voted_at' => '',
        );
    }

    
    
    

    
    public function render_stars_html($product, $variant = 'single') {
        $product_id = $this->resolve_product_id($product);
        if ($product_id <= 0) { return ''; }

        $settings = self::get_settings();
        if (empty($settings['enabled'])) { return ''; }

        
        if ($variant === 'archive' && !empty($settings['aggregate_only_archive'])) {
            return $this->render_aggregate_html($product_id);
        }

        $stats   = $this->get_stats($product_id);
        $current = $this->get_current_rating_for_viewer(
            $product_id,
            get_current_user_id(),
            $this->read_session_token_no_create()
        );

        
        
        
        $pre_rated = !empty($settings['display_full_until_rated']) && $current === 0;

        
        
        
        $caption = $current > 0
            ? $this->build_caption($stats, $settings)
            : (string) $settings['caption_before_rated'];

        $root_classes = 'aieo-rating-stars aieo-rating-stars--' . sanitize_html_class($variant);
        if ($pre_rated) { $root_classes .= ' is-unrated-full'; }
        $stars_html   = '';
        for ($i = 1; $i <= 5; $i++) {
            $is_filled = $pre_rated ? true : ($current >= $i);
            $cls = 'aieo-rating-star';
            if ($is_filled) { $cls .= ' is-filled'; }
            $stars_html .= '<button type="button"'
                . ' class="' . esc_attr($cls) . '"'
                . ' data-aieo-rating-value="' . esc_attr((string) $i) . '"'
                . ' aria-label="' . esc_attr(sprintf(
                    
                    _n('Rate %d star', 'Rate %d stars', $i, 'ai-eshop-optimizer'),
                    $i
                )) . '">'
                . self::star_svg()
                . '</button>';
        }

        
        
        
        $caption_html = '<span class="aieo-rating-caption">' . esc_html($caption) . '</span>';

        
        
        
        
        
        return '<div class="' . esc_attr($root_classes) . '"'
            . ' data-aieo-rating-product="' . esc_attr((string) $product_id) . '"'
            . ' data-aieo-rating-source="' . esc_attr($variant) . '"'
            . ' data-aieo-rating-current="' . esc_attr((string) $current) . '"'
            . ' data-aieo-rating-avg="' . esc_attr((string) $stats['avg']) . '"'
            . ' data-aieo-rating-sum="' . esc_attr((string) $stats['sum']) . '"'
            . ' data-aieo-rating-count="' . esc_attr((string) $stats['count']) . '"'
            . '>'
            .   '<div class="aieo-rating-stars__row" role="radiogroup">' . $stars_html . '</div>'
            .   $caption_html
            . '</div>';
    }

    
    public function render_aggregate_html($product) {
        $product_id = $this->resolve_product_id($product);
        if ($product_id <= 0) { return ''; }

        $settings = self::get_settings();
        if (empty($settings['enabled'])) { return ''; }

        $stats = $this->get_stats($product_id);

        
        
        
        $current = $this->get_current_rating_for_viewer(
            $product_id,
            get_current_user_id(),
            $this->read_session_token_no_create()
        );
        $pre_rated = !empty($settings['display_full_until_rated']) && $current === 0;

        if (!$pre_rated) {
            $min = (int) $settings['min_votes_for_display'];
            if ($stats['count'] < $min) { return ''; }
            if ($stats['count'] <= 0)   { return ''; }
        }

        $avg5    = $pre_rated ? 5.0 : max(0.0, min(5.0, (float) $stats['avg']));
        $percent = round(($avg5 / 5) * 100, 1);
        
        
        $caption = $current > 0
            ? $this->build_caption($stats, $settings)
            : (string) $settings['caption_before_rated'];

        $bg = '';
        for ($i = 1; $i <= 5; $i++) { $bg .= self::star_svg('aieo-rating-aggregate__bg-star'); }
        $fg = '';
        for ($i = 1; $i <= 5; $i++) { $fg .= self::star_svg('aieo-rating-aggregate__fg-star'); }

        $root_classes = 'aieo-rating-aggregate';
        if ($pre_rated) { $root_classes .= ' is-unrated-full'; }

        
        $caption_html = '<span class="aieo-rating-caption">' . esc_html($caption) . '</span>';

        return '<div class="' . esc_attr($root_classes) . '"'
            . ' data-aieo-rating-product="' . esc_attr((string) $product_id) . '"'
            . ' data-aieo-rating-current="' . esc_attr((string) $current) . '"'
            . ' data-aieo-rating-avg="'     . esc_attr((string) $stats['avg']) . '"'
            . ' data-aieo-rating-sum="'     . esc_attr((string) $stats['sum']) . '"'
            . ' data-aieo-rating-count="'   . esc_attr((string) $stats['count']) . '"'
            . '>'
            .   '<div class="aieo-rating-aggregate__stars" aria-hidden="true">'
            .     '<div class="aieo-rating-aggregate__bg">' . $bg . '</div>'
            .     '<div class="aieo-rating-aggregate__fg" style="width:' . esc_attr((string) $percent) . '%">' . $fg . '</div>'
            .   '</div>'
            .   $caption_html
            . '</div>';
    }

    
    private function build_caption($stats, $settings) {
        $count = (int) $stats['count'];
        if ($count <= 0) { return ''; }
        $fmt   = (string) $settings['caption_format'];
        if ($fmt === '') { return ''; }
        $avg   = number_format_i18n((float) $stats['avg'], 1);
        return strtr($fmt, array(
            '%avg5'  => $avg . '/5',
            '%avg'   => $avg,
            '%count' => (string) $count,
        ));
    }

    
    private static function star_svg($extra_class = '') {
        $cls = 'aieo-rating-star-icon' . ($extra_class !== '' ? ' ' . $extra_class : '');
        return '<svg class="' . esc_attr($cls) . '" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">'
            .  '<path d="M12 2.5l2.95 6 6.6.95-4.78 4.66 1.13 6.58L12 17.6l-5.9 3.1 1.13-6.58L2.45 9.45l6.6-.95L12 2.5z" fill="currentColor"/>'
            .  '</svg>';
    }

    private function resolve_product_id($product) {
        if (is_a($product, 'WC_Product')) { return (int) $product->get_id(); }
        return (int) $product;
    }

    
    
    

    public function render_loop_widget() {
        global $product;
        if (!$product || !is_a($product, 'WC_Product')) { return; }
        
        echo $this->render_stars_html($product, 'archive');
    }

    public function render_single_widget() {
        global $product;
        if (!$product || !is_a($product, 'WC_Product')) { return; }
        
        echo $this->render_stars_html($product, 'single');
    }

    public function shortcode_stars($atts = array()) {
        $atts = shortcode_atts(array(
            'product_id' => 0,
            'variant'    => 'single',
        ), $atts, 'aieo_rating_stars');
        $pid = (int) $atts['product_id'];
        if ($pid <= 0) {
            global $product;
            if ($product && is_a($product, 'WC_Product')) { $pid = (int) $product->get_id(); }
        }
        if ($pid <= 0) { return ''; }
        return $this->render_stars_html($pid, sanitize_key((string) $atts['variant']));
    }

    public function shortcode_aggregate($atts = array()) {
        $atts = shortcode_atts(array('product_id' => 0), $atts, 'aieo_rating_aggregate');
        $pid = (int) $atts['product_id'];
        if ($pid <= 0) {
            global $product;
            if ($product && is_a($product, 'WC_Product')) { $pid = (int) $product->get_id(); }
        }
        if ($pid <= 0) { return ''; }
        return $this->render_aggregate_html($pid);
    }

    
    
    

    public function enqueue_assets() {
        if (is_admin()) { return; }
        $settings = self::get_settings();
        if (empty($settings['enabled'])) { return; }

        $current_user_id = get_current_user_id();

        
        
        
        
        
        
        
        $css_uri  = '';
        $css_path = '';
        if (defined('AIEO_RUNTIME_URI') && defined('AIEO_RUNTIME_DIR')) {
            $candidate_path = AIEO_RUNTIME_DIR . 'assets/css/aieo-rating-stars.css';
            if (file_exists($candidate_path)) {
                $css_uri  = AIEO_RUNTIME_URI . 'assets/css/aieo-rating-stars.css';
                $css_path = $candidate_path;
            }
        }
        if (!$css_path) {
            $candidate_path = AIEO_DIR . 'assets/css/aieo-rating-stars.css';
            if (file_exists($candidate_path)) {
                $css_uri  = AIEO_URI . 'assets/css/aieo-rating-stars.css';
                $css_path = $candidate_path;
            }
        }
        $css_ver = $css_path ? (string) filemtime($css_path) : AIEO_VERSION;

        if ($css_uri && $css_path && file_exists($css_path)) {
            wp_enqueue_style('aieo-rating-stars', $css_uri, array(), $css_ver);

            $size_default = (int) $settings['size'];
            $size_mobile  = (int) ($settings['size_mobile'] ?? 0);
            $size_single  = (int) ($settings['size_single'] ?? 0);
            $gap_default  = max(0, min(20, (int) ($settings['gap'] ?? 2)));
            $gap_mobile   = (int) ($settings['gap_mobile'] ?? 0);
            
            $cap_d        = max(8, min(48, (int) ($settings['caption_size'] ?? 13)));
            $cap_m        = max(8, min(48, (int) ($settings['caption_size_mobile'] ?? 0) ?: $cap_d));
            $cap_arch_d   = max(8, min(48, (int) ($settings['caption_size_archive'] ?? 12)));
            $cap_arch_m   = max(8, min(48, (int) ($settings['caption_size_archive_mobile'] ?? 0) ?: $cap_arch_d));

            $css = sprintf(
                ':root{--aieo-rating-empty:%s;--aieo-rating-filled:%s;--aieo-rating-hover:%s;--aieo-rating-pre-rated:%s;--aieo-rating-size:%dpx;--aieo-rating-gap:%dpx;--aieo-rating-cap-fs:%dpx;--aieo-rating-cap-fs-arch:%dpx;}',
                esc_attr((string) $settings['color_empty']),
                esc_attr((string) $settings['color_filled']),
                esc_attr((string) $settings['color_hover']),
                esc_attr((string) $settings['color_pre_rated']),
                $size_default,
                $gap_default,
                $cap_d,
                $cap_arch_d
            );
            
            
            $css .= sprintf(
                '@media (max-width:768px){:root{--aieo-rating-cap-fs:%dpx;--aieo-rating-cap-fs-arch:%dpx;}}',
                $cap_m,
                $cap_arch_m
            );
            
            
            if ($size_single > 0 && $size_single !== $size_default) {
                $css .= sprintf('body.single-product{--aieo-rating-size:%dpx;}', $size_single);
            }
            
            
            
            
            if ($size_mobile > 0 && $size_mobile !== $size_default) {
                $css .= sprintf('@media (max-width:768px){:root{--aieo-rating-size:%dpx;}}', $size_mobile);
            }
            
            
            
            if ($gap_mobile > 0 && $gap_mobile !== $gap_default) {
                $gap_mobile = max(0, min(20, $gap_mobile));
                $css .= sprintf('@media (max-width:768px){:root{--aieo-rating-gap:%dpx;}}', $gap_mobile);
            }
            wp_add_inline_style('aieo-rating-stars', $css);
        }

        
        $payload = array(
            'ajaxUrl'        => admin_url('admin-ajax.php'),
            'restRoot'       => esc_url_raw(rest_url()),
            'restNonce'      => wp_create_nonce('wp_rest'),
            'nonce'          => wp_create_nonce(self::NONCE_ACTION),
            'isLoggedIn'     => $current_user_id > 0,
            'requireLogin'   => !empty($settings['require_login']),
            'loginUrl'       => wp_login_url(),
            'loginRedirectMode' => (string) $settings['login_redirect_mode'],
            'allowClear'     => !empty($settings['allow_clear']),
            'captionFormat'      => (string) $settings['caption_format'],
            'captionBeforeRated' => (string) $settings['caption_before_rated'],
            'minVotesForDisplay' => (int) $settings['min_votes_for_display'],
            'currentRatings' => array(), 
        );

        $iapi_on = method_exists('AIEO_DMM', 'is_iapi_enabled') && AIEO_DMM::is_iapi_enabled('ratings');

        
        
        
        $runtime_js_path = '';
        $runtime_js_uri  = '';
        if (defined('AIEO_RUNTIME_DIR') && defined('AIEO_RUNTIME_URI')) {
            $cand = AIEO_RUNTIME_DIR . 'assets/js/aieo-rating-stars.js';
            if (file_exists($cand)) {
                $runtime_js_path = $cand;
                $runtime_js_uri  = AIEO_RUNTIME_URI . 'assets/js/aieo-rating-stars.js';
            }
        }
        if (!$runtime_js_path) {
            $cand = AIEO_DIR . 'assets/js/aieo-rating-stars.js';
            if (file_exists($cand)) {
                $runtime_js_path = $cand;
                $runtime_js_uri  = AIEO_URI . 'assets/js/aieo-rating-stars.js';
            }
        }
        $iapi_js_path = '';
        $iapi_js_uri  = '';
        if (defined('AIEO_RUNTIME_DIR') && defined('AIEO_RUNTIME_URI')) {
            $cand = AIEO_RUNTIME_DIR . 'assets/js/aieo-rating-stars-iapi.js';
            if (file_exists($cand)) {
                $iapi_js_path = $cand;
                $iapi_js_uri  = AIEO_RUNTIME_URI . 'assets/js/aieo-rating-stars-iapi.js';
            }
        }
        if (!$iapi_js_path) {
            $cand = AIEO_DIR . 'assets/js/aieo-rating-stars-iapi.js';
            if (file_exists($cand)) {
                $iapi_js_path = $cand;
                $iapi_js_uri  = AIEO_URI . 'assets/js/aieo-rating-stars-iapi.js';
            }
        }

        if ($iapi_on
            && function_exists('wp_enqueue_script_module')
            && $iapi_js_path && file_exists($iapi_js_path)
        ) {
            wp_register_script_module(
                '@aieo/rating-stars-iapi',
                $iapi_js_uri,
                array('@wordpress/interactivity', '@aieo/iapi-bootstrap'),
                (string) filemtime($iapi_js_path)
            );
            wp_enqueue_script_module('@aieo/rating-stars-iapi');

            
            add_action('wp_print_footer_scripts', static function () use ($payload) {
                echo '<script id="aieo-ratings-data">'
                    . 'window.aieoRatings = ' . wp_json_encode($payload) . ';'
                    . '</script>' . "\n";
            }, 5);
        } elseif ($runtime_js_path && file_exists($runtime_js_path)) {
            $js_ver = (string) filemtime($runtime_js_path);
            wp_enqueue_script('aieo-rating-stars', $runtime_js_uri, array('jquery'), $js_ver, true);
            wp_localize_script('aieo-rating-stars', 'aieoRatings', $payload);
        }
    }

    
    
    

    public function register_admin_menu() {
        add_submenu_page(
            'ai-eshop-optimizer',
            __('Star Ratings', 'ai-eshop-optimizer'),
            '⭐ ' . __('Star Ratings', 'ai-eshop-optimizer'),
            AIEO_CAPABILITY,
            'aieo-ratings',
            array($this, 'render_admin_page')
        );
    }

    public function render_admin_page() {
        if (!current_user_can(AIEO_CAPABILITY)) {
            wp_die(esc_html__('You do not have permission to access this page.', 'ai-eshop-optimizer'));
        }
        $s     = self::get_settings();
        $saved = isset($_GET['aieo_ratings_saved']) && $_GET['aieo_ratings_saved'] === '1';
        ?>
        <div class="wrap aieo-admin">
            <h1>⭐ <?php esc_html_e('Star Ratings', 'ai-eshop-optimizer'); ?></h1>
            <p class="description">
                <?php esc_html_e('5-state product rating widget with per-product aggregate. Anonymous voters are scoped by the aieo_rating_session cookie.', 'ai-eshop-optimizer'); ?>
            </p>
            <?php if ($saved): ?>
                <div class="notice notice-success is-dismissible"><p><?php esc_html_e('Settings saved.', 'ai-eshop-optimizer'); ?></p></div>
            <?php endif; ?>
            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                <input type="hidden" name="action" value="aieo_ratings_save_settings">
                <?php wp_nonce_field('aieo_ratings_save_settings'); ?>
                <table class="form-table" role="presentation">
                    <?php $this->row_checkbox('enabled',                $s, __('Enable Star Ratings', 'ai-eshop-optimizer'), __('Master switch for the module.', 'ai-eshop-optimizer')); ?>
                    <?php $this->row_checkbox('require_login',          $s, __('Require login to vote', 'ai-eshop-optimizer'), __('When ON, anonymous shoppers see the widget but clicks are gated.', 'ai-eshop-optimizer')); ?>
                    <?php $this->row_select('login_redirect_mode',      $s, __('Login gate behavior', 'ai-eshop-optimizer'), array(
                        'redirect' => __('Redirect to login page', 'ai-eshop-optimizer'),
                        'event'    => __('Emit aieo:rating:login_required event (theme-handled)', 'ai-eshop-optimizer'),
                    )); ?>
                    <?php $this->row_checkbox('display_archive',        $s, __('Display on archive cards', 'ai-eshop-optimizer'), ''); ?>
                    <?php $this->row_checkbox('display_single',         $s, __('Display on single product page', 'ai-eshop-optimizer'), ''); ?>
                    <?php $this->row_checkbox('aggregate_only_archive', $s, __('Aggregate only on archive', 'ai-eshop-optimizer'), __('When ON, archive cards show the read-only aggregate (no interactive widget).', 'ai-eshop-optimizer')); ?>
                    <?php  ?>
                    <?php $this->row_text('caption_format',             $s, __('Caption format', 'ai-eshop-optimizer'), __('Tokens: %avg, %count, %avg5. Empty = hide caption.', 'ai-eshop-optimizer')); ?>
                    <?php $this->row_number('min_votes_for_display',    $s, __('Minimum votes to show aggregate', 'ai-eshop-optimizer'), __('Hide aggregate until N votes — avoids "5.0 (1)" being misleading.', 'ai-eshop-optimizer')); ?>
                    <?php $this->row_color('color_empty',               $s, __('Empty star color', 'ai-eshop-optimizer')); ?>
                    <?php $this->row_color('color_filled',              $s, __('Filled star color', 'ai-eshop-optimizer')); ?>
                    <?php $this->row_color('color_hover',               $s, __('Hover color', 'ai-eshop-optimizer')); ?>
                    <?php $this->row_number('size',                     $s, __('Star size — desktop (px)', 'ai-eshop-optimizer'), __('Default size used everywhere. The two overrides below kick in on top of this.', 'ai-eshop-optimizer')); ?>
                    <?php $this->row_number('size_mobile',              $s, __('Star size — mobile (px)', 'ai-eshop-optimizer'), __('Applied on viewports ≤ 768px wide. 0 = inherit desktop size.', 'ai-eshop-optimizer')); ?>
                    <?php $this->row_number('size_single',              $s, __('Star size — single-product page (px)', 'ai-eshop-optimizer'), __('Applied only on the single-product page. 0 = inherit desktop size. On mobile single-product pages the mobile size wins.', 'ai-eshop-optimizer')); ?>
                    <?php $this->row_checkbox('allow_clear',            $s, __('Allow clear', 'ai-eshop-optimizer'), __('Re-click the active star to clear the rating.', 'ai-eshop-optimizer')); ?>
                </table>
                <p class="submit">
                    <button type="submit" class="button button-primary"><?php esc_html_e('Save Changes', 'ai-eshop-optimizer'); ?></button>
                </p>
            </form>
        </div>
        <?php
    }

    private function row_checkbox($key, $s, $label, $desc) {
        ?>
        <tr><th scope="row"><?php echo esc_html($label); ?></th><td>
            <label>
                <input type="checkbox" name="aieo_ratings[<?php echo esc_attr($key); ?>]" value="1" <?php checked(!empty($s[$key])); ?>>
                <?php echo esc_html($desc); ?>
            </label>
        </td></tr>
        <?php
    }
    private function row_text($key, $s, $label, $desc) {
        ?>
        <tr><th scope="row"><?php echo esc_html($label); ?></th><td>
            <input type="text" class="regular-text" name="aieo_ratings[<?php echo esc_attr($key); ?>]" value="<?php echo esc_attr((string) $s[$key]); ?>">
            <p class="description"><?php echo esc_html($desc); ?></p>
        </td></tr>
        <?php
    }
    private function row_number($key, $s, $label, $desc) {
        ?>
        <tr><th scope="row"><?php echo esc_html($label); ?></th><td>
            <input type="number" min="0" name="aieo_ratings[<?php echo esc_attr($key); ?>]" value="<?php echo esc_attr((string) (int) $s[$key]); ?>">
            <p class="description"><?php echo esc_html($desc); ?></p>
        </td></tr>
        <?php
    }
    private function row_color($key, $s, $label) {
        ?>
        <tr><th scope="row"><?php echo esc_html($label); ?></th><td>
            <input type="text" name="aieo_ratings[<?php echo esc_attr($key); ?>]" value="<?php echo esc_attr((string) $s[$key]); ?>" placeholder="#000000">
        </td></tr>
        <?php
    }
    private function row_select($key, $s, $label, $opts) {
        ?>
        <tr><th scope="row"><?php echo esc_html($label); ?></th><td>
            <select name="aieo_ratings[<?php echo esc_attr($key); ?>]">
                <?php foreach ($opts as $val => $text) : ?>
                    <option value="<?php echo esc_attr($val); ?>" <?php selected((string) $s[$key], (string) $val); ?>><?php echo esc_html($text); ?></option>
                <?php endforeach; ?>
            </select>
        </td></tr>
        <?php
    }

    public function handle_save_settings() {
        if (!current_user_can(AIEO_CAPABILITY)) {
            wp_die(esc_html__('Insufficient permissions.', 'ai-eshop-optimizer'));
        }
        check_admin_referer('aieo_ratings_save_settings');

        $in   = isset($_POST['aieo_ratings']) && is_array( wp_unslash( $_POST['aieo_ratings'] ) ) ? wp_unslash($_POST['aieo_ratings']) : array();
        $cur  = self::get_settings();
        $bool_keys = array('enabled', 'require_login', 'display_archive', 'display_single', 'aggregate_only_archive', 'allow_clear');
        foreach ($bool_keys as $k) {
            $cur[$k] = !empty($in[$k]) ? 1 : 0;
        }
        if (isset($in['login_redirect_mode'])) {
            $v = sanitize_key((string) $in['login_redirect_mode']);
            $cur['login_redirect_mode'] = in_array($v, array('redirect', 'event'), true) ? $v : 'redirect';
        }
        if (isset($in['caption_format'])) {
            $cur['caption_format'] = sanitize_text_field((string) $in['caption_format']);
        }
        if (isset($in['min_votes_for_display'])) {
            $cur['min_votes_for_display'] = max(0, (int) $in['min_votes_for_display']);
        }
        if (isset($in['size'])) {
            $cur['size'] = max(8, min(96, (int) $in['size']));
        }
        
        foreach (array('size_mobile', 'size_single') as $k) {
            if (isset($in[$k])) {
                $v = (int) $in[$k];
                if ($v <= 0) { $cur[$k] = 0; }
                else         { $cur[$k] = max(8, min(96, $v)); }
            }
        }
        foreach (array('color_empty', 'color_filled', 'color_hover') as $k) {
            if (isset($in[$k])) {
                $v = sanitize_hex_color((string) $in[$k]);
                if ($v !== null && $v !== '') { $cur[$k] = $v; }
            }
        }

        update_option(self::SETTINGS_OPTION, $cur, false);

        wp_safe_redirect(add_query_arg(
            array('page' => 'aieo-ratings', 'aieo_ratings_saved' => '1'),
            admin_url('admin.php')
        ));
        exit;
    }
}
}
