<?php

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

if ( class_exists( 'AIEO_Attribution' ) ) {
    return;
}

class AIEO_Attribution {

    const SCHEMA_VERSION      = '2';
    const SCHEMA_OPTION       = 'aieo_attribution_schema_version';
    const RECO_SCHEMA_VERSION = '3';
    const RECO_SCHEMA_OPTION  = 'aieo_reco_events_schema_version';

    
    const RECO_EVENT_TYPES = array('impression', 'reco_click', 'ei_impression', 'ei_dismiss', 'promo_impression', 'promo_click');

    
    const RECO_NON_PRODUCT_EVENT_TYPES = array('ei_impression', 'ei_dismiss');

    
    const VALID_SOURCES = array(
        'archive',
        
        
        
        'blog_post',
        'home',
        'single_product',
        'cart',
        'checkout',
        'product_upsells',
        'product_crosssells',
        'product_needs',
        'product_related',
        'product_recently_viewed',
        'sidecart_recently_viewed',
        'sidecart_might_like',
        'sidecart_may_also_like',
        'sidecart_product_needs',
        'sidecart_favorites',
        'sidecart_quickview',
        'sticky_atc',
        
        
        
        
        
        'complementary',
        
        
        
        
        
        
        
        
        
        
        'tolstoy',
        
        
        
        'unknown',
    );

    
    private static $instance = null;

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

    private function __construct() {
        
        
        
        if ( ! defined( 'AIEO_RUNTIME_OWNS_AJAX' ) ) {
            add_action('wp_ajax_aieo_attribution_log',        array($this, 'ajax_log'));
            add_action('wp_ajax_nopriv_aieo_attribution_log', array($this, 'ajax_log'));
        }

        
        
        
        
        
        
        if ( ! defined( 'AIEO_RUNTIME_OWNS_AJAX' ) ) {
            add_action('wp_ajax_aieo_reco_log_batch',        array($this, 'ajax_reco_batch'));
            add_action('wp_ajax_nopriv_aieo_reco_log_batch', array($this, 'ajax_reco_batch'));
        }

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

        
        
        
        
        
        
        
        
        
        
        
        
        
        if ( ! defined( 'AIEO_RUNTIME_OWNS_AJAX' ) ) {
            add_action('woocommerce_checkout_order_processed', array($this, 'on_order_processed'), 20, 2);
        }
        add_action('woocommerce_thankyou',                 array($this, 'on_thankyou'),       20, 1);

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

    
    
    

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

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

    public function maybe_install() {
        $installed = get_option(self::SCHEMA_OPTION, '');
        if ($installed !== self::SCHEMA_VERSION) {
            self::install();
            update_option(self::SCHEMA_OPTION, self::SCHEMA_VERSION, false);
        }
        $reco_installed = get_option(self::RECO_SCHEMA_OPTION, '');
        if ($reco_installed !== self::RECO_SCHEMA_VERSION) {
            self::install_reco();
            update_option(self::RECO_SCHEMA_OPTION, self::RECO_SCHEMA_VERSION, false);
        }
        
        
        
        
        self::ensure_session_channel_columns();

        
        
        self::ensure_badge_context_column();
    }

    
    public static function ensure_badge_context_column() {
        global $wpdb;
        $tbl = self::get_table_name();
        if ((string) $wpdb->get_var("SHOW TABLES LIKE '{$tbl}'") !== $tbl) {
            return;
        }
        
        $bcs = $wpdb->get_var("SHOW COLUMNS FROM {$tbl} LIKE 'badge_context_snapshot'");
        if (!$bcs) {
            
            
            
            $wpdb->query("ALTER TABLE {$tbl} ADD COLUMN badge_context_snapshot LONGTEXT NULL DEFAULT NULL AFTER source_meta");
        }
        
        
        $virtuals = array(
            'bcs_was_on_sale'      => "TINYINT GENERATED ALWAYS AS (CAST(JSON_VALUE(badge_context_snapshot, '\$.was_on_sale') AS UNSIGNED)) VIRTUAL",
            'bcs_is_new'           => "TINYINT GENERATED ALWAYS AS (CAST(JSON_VALUE(badge_context_snapshot, '\$.is_new_product') AS UNSIGNED)) VIRTUAL",
            'bcs_gift_offer_id'    => "INT     GENERATED ALWAYS AS (CAST(JSON_VALUE(badge_context_snapshot, '\$.eligible_gift_offer.offer_id') AS UNSIGNED)) VIRTUAL",
            'bcs_monthly_offer_id' => "INT     GENERATED ALWAYS AS (CAST(JSON_VALUE(badge_context_snapshot, '\$.monthly_offer.offer_id') AS UNSIGNED)) VIRTUAL",
        );
        foreach ($virtuals as $name => $def) {
            $exists = $wpdb->get_var("SHOW COLUMNS FROM {$tbl} LIKE '{$name}'");
            if (!$exists) {
                
                
                
                
                @$wpdb->query("ALTER TABLE {$tbl} ADD COLUMN {$name} {$def}");
            }
        }
        
        $idx_names = array(
            'idx_bcs_sale' => 'bcs_was_on_sale',
            'idx_bcs_new'  => 'bcs_is_new',
            'idx_bcs_gift' => 'bcs_gift_offer_id',
            'idx_bcs_mo'   => 'bcs_monthly_offer_id',
        );
        foreach ($idx_names as $idx => $col) {
            $exists = $wpdb->get_var("SHOW INDEX FROM {$tbl} WHERE Key_name = '{$idx}'");
            if (!$exists) {
                @$wpdb->query("ALTER TABLE {$tbl} ADD INDEX {$idx} ({$col})");
            }
        }
    }

    
    public static function capture_badge_context($product_id, $variation_id = 0, $user_id = 0) {
        $product_id   = (int) $product_id;
        $variation_id = (int) $variation_id;
        $priced_pid   = $variation_id > 0 ? $variation_id : $product_id;

        $audience_key = class_exists('AIEO_Identity_Bridge')
            ? AIEO_Identity_Bridge::audience_key_for_current_user()
            : 'guest';

        $snap = array(
            'schema_v'     => 1,
            'captured_at'  => gmdate('c'),
            'audience_key' => $audience_key,
            'priced_pid'   => $priced_pid,
        );

        
        if (class_exists('AIEO_Effective_Prices')) {
            $row = AIEO_Effective_Prices::get_for($product_id, $audience_key);
            if (is_array($row)) {
                $snap['was_on_sale']        = !empty($row['was_on_sale']) ? 1 : 0;
                $snap['sale_discount_pct']  = isset($row['sale_discount_pct']) ? $row['sale_discount_pct'] : null;
                $snap['regular_price']      = isset($row['base_price']) ? (float) $row['base_price'] : null;
                $snap['effective_price']    = isset($row['sale_price']) && $row['sale_price'] !== null
                    ? (float) $row['sale_price'] : (isset($row['base_price']) ? (float) $row['base_price'] : null);
                $snap['role_price_applied'] = !empty($row['role_price_applied']) ? 1 : 0;
                $snap['monthly_offer'] = array(
                    'active'   => !empty($row['monthly_offer_active']) ? 1 : 0,
                    'offer_id' => null,
                );
                $snap['source_rule'] = array(
                    'type' => isset($row['source_rule_type']) ? $row['source_rule_type'] : null,
                    'id'   => isset($row['source_rule_id']) ? (int) $row['source_rule_id'] : null,
                );
            }
        }

        
        
        
        if (class_exists('AIEO_DMM_Brand_Discounts')) {
            $snap['is_new_product'] = AIEO_DMM_Brand_Discounts::is_new_product($product_id) ? 1 : 0;
            $post_time = get_post_time('U', true, $product_id);
            $snap['days_since_publish'] = $post_time
                ? (int) floor((time() - (int) $post_time) / DAY_IN_SECONDS)
                : null;
        }

        
        
        
        if (class_exists('AIEO_Effective_Eligibility')) {
            foreach (array('brand_gift', 'product_label', 'complementary') as $surface) {
                $row = AIEO_Effective_Eligibility::get_for($surface, $audience_key, $product_id);
                if (!is_array($row) || empty($row['eligible'])) { continue; }
                switch ($surface) {
                    case 'brand_gift':
                        $snap['eligible_gift_offer'] = array(
                            'offer_id' => isset($row['offer_id']) ? (int) $row['offer_id'] : null,
                            'label'    => isset($row['payload']['label']) ? (string) $row['payload']['label'] : '',
                            
                            
                            
                            'qualified_at_addtime' => 0,
                        );
                        break;
                    case 'product_label':
                        if (!isset($row['payload']['rule'])) { break; }
                        $snap['product_label'] = array(
                            'rule'  => (string) $row['payload']['rule'],
                            'label' => isset($row['payload']['label']) ? (string) $row['payload']['label'] : '',
                        );
                        break;
                    case 'complementary':
                        $snap['complementary'] = array(
                            'recipe_id' => isset($row['offer_id']) ? (int) $row['offer_id'] : null,
                            'count'     => isset($row['payload']['count']) ? (int) $row['payload']['count'] : 0,
                        );
                        break;
                }
            }
        }

        
        
        
        if (!empty($snap['monthly_offer']['active']) && class_exists('AIEO_DMM_Brand_Discounts')) {
            $batch = get_post_meta($product_id, AIEO_DMM_Brand_Discounts::META_MONTHLY_BATCH, true);
            if (!empty($batch)) {
                $snap['monthly_offer']['offer_id'] = is_numeric($batch) ? (int) $batch : (string) $batch;
            }
        }

        return $snap;
    }

    
    public static function ensure_session_channel_columns() {
        global $wpdb;
        $tbl = $wpdb->prefix . 'aieo_sessions_statics';
        
        
        if ((string) $wpdb->get_var("SHOW TABLES LIKE '{$tbl}'") !== $tbl) {
            return;
        }
        $cols = array(
            'entry_channel'  => "VARCHAR(40)  NOT NULL DEFAULT '' AFTER referrer",
            'entry_campaign' => "VARCHAR(120) NOT NULL DEFAULT '' AFTER entry_channel",
            'entry_medium'   => "VARCHAR(40)  NOT NULL DEFAULT '' AFTER entry_campaign",
            'entry_term'     => "VARCHAR(80)  NOT NULL DEFAULT '' AFTER entry_medium",
            'entry_content'  => "VARCHAR(80)  NOT NULL DEFAULT '' AFTER entry_term",
            
            
            'entry_source'   => "VARCHAR(80)  NOT NULL DEFAULT '' AFTER entry_channel",
            
            
            
            'entry_click_id'       => "VARCHAR(255) NOT NULL DEFAULT '' AFTER entry_content",
            'entry_ad_id'          => "VARCHAR(40)  NOT NULL DEFAULT '' AFTER entry_click_id",
            'entry_adset_id'       => "VARCHAR(40)  NOT NULL DEFAULT '' AFTER entry_ad_id",
            'entry_ad_campaign_id' => "VARCHAR(40)  NOT NULL DEFAULT '' AFTER entry_adset_id",
        );
        
        
        
        
        
        $prev_suppress = $wpdb->suppress_errors(true);
        foreach ($cols as $name => $def) {
            $exists = $wpdb->get_var("SHOW COLUMNS FROM {$tbl} LIKE '{$name}'");
            if (!$exists) {
                $wpdb->query("ALTER TABLE {$tbl} ADD COLUMN {$name} {$def}");
            }
        }
        
        
        $idx = $wpdb->get_var("SHOW INDEX FROM {$tbl} WHERE Key_name = 'idx_entry_channel'");
        if (!$idx) {
            $wpdb->query("ALTER TABLE {$tbl} ADD INDEX idx_entry_channel (entry_channel)");
        }
        $wpdb->suppress_errors($prev_suppress);

        
        
        
        
        
        
        self::reconcile_session_id_collation('utf8mb4_uca1400_ai_ci');
        
        
        self::reconcile_column_collation('email', 'utf8mb4_uca1400_ai_ci');
    }

    
    public static function reconcile_session_id_collation(string $target): void {
        self::reconcile_column_collation('session_id', $target);
    }

    
    public static function reconcile_column_collation(string $column, string $target): void {
        global $wpdb;
        $column = preg_replace('/[^a-z0-9_]/i', '', $column);
        if ($column === '') { return; }

        
        
        
        
        
        
        
        $supported = (string) $wpdb->get_var($wpdb->prepare(
            "SELECT COLLATION_NAME FROM information_schema.COLLATIONS
             WHERE COLLATION_NAME = %s LIMIT 1",
            $target
        ));
        if ($supported === '') {
            $addcart_tbl = $wpdb->prefix . 'aieo_addcart_events';
            $fallback = (string) $wpdb->get_var($wpdb->prepare(
                "SELECT COLLATION_NAME FROM information_schema.COLUMNS
                 WHERE TABLE_SCHEMA = DATABASE()
                   AND TABLE_NAME = %s
                   AND COLUMN_NAME = %s",
                $addcart_tbl,
                $column
            ));
            if ($fallback === '' || $fallback === $target) {
                
                
                return;
            }
            $target = $fallback;
        }

        $prefix_like = $wpdb->esc_like($wpdb->prefix . 'aieo_') . '%';

        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT TABLE_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT
             FROM information_schema.COLUMNS
             WHERE TABLE_SCHEMA = DATABASE()
               AND TABLE_NAME LIKE %s
               AND COLUMN_NAME = %s
               AND COLLATION_NAME <> %s",
            $prefix_like,
            $column,
            $target
        ), ARRAY_A);

        if (empty($rows)) { return; }

        foreach ($rows as $r) {
            $tbl  = $r['TABLE_NAME'];
            $type = $r['COLUMN_TYPE']; 
            $null = strtoupper($r['IS_NULLABLE']) === 'YES' ? 'NULL' : 'NOT NULL';
            
            if ($r['COLUMN_DEFAULT'] === null) {
                $default = ($null === 'NOT NULL') ? "DEFAULT ''" : '';
            } else {
                $default = "DEFAULT '" . esc_sql((string) $r['COLUMN_DEFAULT']) . "'";
            }
            $wpdb->query(
                "ALTER TABLE `{$tbl}` MODIFY `{$column}` {$type}
                 CHARACTER SET utf8mb4 COLLATE {$target}
                 {$null} {$default}"
            );
        }
    }

    public static function install() {
        global $wpdb;
        $table   = self::get_table_name();
        $charset = $wpdb->get_charset_collate();
        $sql = "CREATE TABLE {$table} (
            id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            event_id varchar(36) NOT NULL DEFAULT '',
            session_id varchar(64) NOT NULL DEFAULT '',
            user_id bigint(20) UNSIGNED NOT NULL DEFAULT 0,
            product_id bigint(20) UNSIGNED NOT NULL,
            variation_id bigint(20) UNSIGNED NOT NULL DEFAULT 0,
            quantity int NOT NULL DEFAULT 1,
            unit_price decimal(18,4) NOT NULL DEFAULT 0.0000,
            source varchar(40) NOT NULL DEFAULT 'unknown',
            source_pid bigint(20) UNSIGNED NOT NULL DEFAULT 0,
            source_meta text NULL,
            cart_position int NOT NULL DEFAULT 0,
            ts_added datetime NOT NULL,
            ts_viewed datetime NULL DEFAULT NULL,
            order_id bigint(20) UNSIGNED NOT NULL DEFAULT 0,
            ga_sent tinyint(1) NOT NULL DEFAULT 0,
            PRIMARY KEY  (id),
            UNIQUE KEY event_id (event_id),
            KEY session_id (session_id),
            KEY product_id (product_id),
            KEY source (source),
            KEY ts_added (ts_added),
            KEY order_id (order_id),
            KEY user_id (user_id)
        ) {$charset};";
        if (!function_exists('dbDelta')) {
            require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        }
        dbDelta($sql);
    }

    
    public static function install_reco() {
        global $wpdb;
        $table   = self::get_reco_table_name();
        $charset = $wpdb->get_charset_collate();
        
        
        
        
        
        
        $sql = "CREATE TABLE {$table} (
            id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            event_id varchar(36) NOT NULL DEFAULT '',
            session_id varchar(64) NOT NULL DEFAULT '',
            user_id bigint(20) UNSIGNED NOT NULL DEFAULT 0,
            event_type varchar(16) NOT NULL DEFAULT 'impression',
            target_pid bigint(20) UNSIGNED NOT NULL,
            anchor_pid bigint(20) UNSIGNED NOT NULL DEFAULT 0,
            anchor_term_id bigint(20) UNSIGNED NOT NULL DEFAULT 0,
            anchor_term_tax varchar(32) NOT NULL DEFAULT '',
            surface varchar(40) NOT NULL DEFAULT 'unknown',
            position smallint UNSIGNED NOT NULL DEFAULT 0,
            ts datetime NOT NULL,
            PRIMARY KEY  (id),
            UNIQUE KEY event_id (event_id),
            KEY session_target (session_id, target_pid),
            KEY surface (surface),
            KEY anchor_pid (anchor_pid),
            KEY anchor_term_id (anchor_term_id),
            KEY ts (ts)
        ) {$charset};";
        if (!function_exists('dbDelta')) {
            require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        }
        dbDelta($sql);
    }

    
    
    

    
    public function enqueue_assets() {
        if (is_admin()) { return; }
        
        
        
        
        if (!class_exists('WooCommerce')) { return; }
        if (!apply_filters('aieo_attribution_client_enabled', true)) { return; }
        $handle = 'aieo-attribution-client';
        $rel    = 'assets/js/aieo-attribution-client.js';
        $url    = plugins_url($rel, AIEO_FILE);
        $path   = dirname(AIEO_FILE) . '/' . $rel;
        $ver    = file_exists($path) ? filemtime($path) : (defined('AIEO_VERSION') ? AIEO_VERSION : false);
        
        
        
        
        
        $archive_context = $this->resolve_archive_context();

        
        
        
        $conv        = class_exists('AIEO_DMM_Conversions') ? AIEO_DMM_Conversions::get_config() : array();
        $ga4_browser = ( isset($conv['ga_source']) && 'aieo' === $conv['ga_source']
            && ( ! isset($conv['ga_send_browser']) || 'yes' === $conv['ga_send_browser'] ) );

        
        
        wp_enqueue_script($handle, $url, array(), $ver, array('in_footer' => true, 'strategy' => 'defer'));
        wp_localize_script($handle, 'aieoAttribution', array(
            'enabled'     => true,
            'ajaxUrl'     => admin_url('admin-ajax.php'),
            'currency'    => function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : 'EUR',
            'archive'     => $archive_context,
            'ga4_enabled' => $ga4_browser,
        ));

        
        
        
        $reco_handle = 'aieo-reco-observer';
        $reco_rel    = 'assets/js/aieo-reco-observer.js';
        $reco_url    = plugins_url($reco_rel, AIEO_FILE);
        $reco_path   = dirname(AIEO_FILE) . '/' . $reco_rel;
        $reco_ver    = file_exists($reco_path) ? filemtime($reco_path) : (defined('AIEO_VERSION') ? AIEO_VERSION : false);
        
        wp_enqueue_script($reco_handle, $reco_url, array(), $reco_ver, array('in_footer' => true, 'strategy' => 'defer'));
        wp_localize_script($reco_handle, 'aieoRecoObserver', array(
            'enabled'         => true,
            'ajaxUrl'         => admin_url('admin-ajax.php'),
            'flushIntervalMs' => 5000,    
            'maxBatchSize'    => 40,       
            'archive'         => $archive_context,
        ));
    }

    
    private function resolve_archive_context() {
        if (!function_exists('is_tax') || !function_exists('get_queried_object')) {
            return null;
        }
        
        
        
        $brand_taxonomies = array('product_brand', 'pwb-brand', 'yith_product_brand');
        
        
        
        
        if (!is_tax()) { return null; }
        $term = get_queried_object();
        if (!$term || empty($term->term_id) || empty($term->taxonomy)) { return null; }
        $tax = (string) $term->taxonomy;
        $tax_obj = function_exists('get_taxonomy') ? get_taxonomy($tax) : null;
        $object_types = ($tax_obj && !empty($tax_obj->object_type)) ? (array) $tax_obj->object_type : array();
        $is_brand = in_array($tax, $brand_taxonomies, true);
        if (!$is_brand && !in_array('product', $object_types, true)) {
            return null;
        }
        
        
        if ($is_brand) { $tax = 'product_brand'; }
        return array(
            'term_id' => (int) $term->term_id,
            'tax'     => $tax,
            'slug'    => (string) $term->slug,
            'name'    => (string) $term->name,
        );
    }

    
    
    

    
    public function ajax_log() {
        
        
        if (!headers_sent()) {
            @header('Connection: close');
            @header('Content-Length: 0');
        }
        while (ob_get_level() > 0) { ob_end_clean(); }

        $this->log_event(array(
            'event_id'     => isset($_POST['event_id'])     ? sanitize_text_field((string) wp_unslash($_POST['event_id'])) : '',
            'product_id'   => isset($_POST['product_id'])   ? absint(wp_unslash($_POST['product_id'])) : 0,
            'variation_id' => isset($_POST['variation_id']) ? absint(wp_unslash($_POST['variation_id'])) : 0,
            'quantity'     => isset($_POST['quantity'])     ? max(1, absint(wp_unslash($_POST['quantity']))) : 1,
            'unit_price'   => isset($_POST['unit_price'])   ? (float) sanitize_text_field(wp_unslash($_POST['unit_price'])) : 0.0,
            'source'       => isset($_POST['source'])       ? sanitize_key((string) wp_unslash($_POST['source'])) : 'unknown',
            'source_pid'   => isset($_POST['source_pid'])   ? absint(wp_unslash($_POST['source_pid'])) : 0,
            'source_meta'  => isset($_POST['source_meta'])
                ? (is_array($_POST['source_meta'])
                    ? map_deep(wp_unslash($_POST['source_meta']), 'sanitize_text_field')
                    : sanitize_textarea_field((string) wp_unslash($_POST['source_meta'])))
                : null,
        ));

        
        
        status_header(204);
        wp_die('', '', array('response' => 204));
    }

    
    
    

    
    public function ajax_reco_batch() {
        if (!headers_sent()) {
            @header('Connection: close');
            @header('Content-Length: 0');
        }
        while (ob_get_level() > 0) { ob_end_clean(); }

        
        $raw = isset($_POST['events']) ? wp_unslash((string) $_POST['events']) : '';
        $events = $raw !== '' ? json_decode($raw, true) : null;
        if (is_array($events) && !empty($events)) {
            $this->log_reco_events($events);
        }

        status_header(204);
        wp_die('', '', array('response' => 204));
    }

    
    private function log_reco_events(array $events) {
        global $wpdb;
        $table        = self::get_reco_table_name();
        $session_id   = sanitize_text_field($this->get_session_id());
        $user_id      = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
        $request_time = time();

        $rows = array();
        $placeholders = array();
        $cap = 100; 
        $i = 0;
        foreach ($events as $e) {
            if ($i++ >= $cap) { break; }
            if (!is_array($e)) { continue; }
            $type = isset($e['type']) ? (string) $e['type'] : '';
            if (!in_array($type, self::RECO_EVENT_TYPES, true)) { continue; }
            $target_pid = isset($e['target_pid']) ? (int) $e['target_pid'] : 0;
            
            
            
            if ($target_pid <= 0 && !in_array($type, self::RECO_NON_PRODUCT_EVENT_TYPES, true)) { continue; }
            $anchor_pid = isset($e['anchor_pid']) ? (int) $e['anchor_pid'] : 0;
            $surface    = isset($e['surface']) ? sanitize_key((string) $e['surface']) : 'unknown';
            $event_id   = isset($e['event_id']) ? (string) $e['event_id'] : '';
            if ($event_id === '' || strlen($event_id) > 36) {
                $event_id = $this->generate_uuid_v4();
            }
            
            
            $offset_ms = isset($e['ts_offset_ms']) ? (int) $e['ts_offset_ms'] : 0;
            if ($offset_ms < 0) { $offset_ms = 0; }
            if ($offset_ms > 6 * HOUR_IN_SECONDS * 1000) { $offset_ms = 0; }
            $ts = gmdate('Y-m-d H:i:s', $request_time - intdiv($offset_ms, 1000));

            $position = isset($e['position']) ? max(0, min(999, (int) $e['position'])) : 0;
            $anchor_term_id  = isset($e['anchor_term_id']) ? (int) $e['anchor_term_id'] : 0;
            $anchor_term_tax = isset($e['anchor_term_tax'])
                ? sanitize_key((string) $e['anchor_term_tax'])
                : '';
            
            
            $allowed_tax = array('product_cat', 'product_tag', 'product_brand');
            if ($anchor_term_tax !== '' && !in_array($anchor_term_tax, $allowed_tax, true)) {
                $anchor_term_tax = '';
                $anchor_term_id  = 0;
            }

            $rows[] = array(
                'event_id'        => $event_id,
                'session_id'      => $session_id,
                'user_id'         => $user_id,
                'event_type'      => $type,
                'target_pid'      => $target_pid,
                'anchor_pid'      => $anchor_pid,
                'anchor_term_id'  => $anchor_term_id,
                'anchor_term_tax' => $anchor_term_tax,
                'surface'         => $surface,
                'position'        => $position,
                'ts'              => $ts,
            );
            $placeholders[] = '(%s, %s, %d, %s, %d, %d, %d, %s, %s, %d, %s)';
        }

        if (empty($rows)) { return; }

        $values = array();
        foreach ($rows as $r) {
            $values[] = $r['event_id'];
            $values[] = $r['session_id'];
            $values[] = $r['user_id'];
            $values[] = $r['event_type'];
            $values[] = $r['target_pid'];
            $values[] = $r['anchor_pid'];
            $values[] = $r['anchor_term_id'];
            $values[] = $r['anchor_term_tax'];
            $values[] = $r['surface'];
            $values[] = $r['position'];
            $values[] = $r['ts'];
        }
        $sql = "INSERT IGNORE INTO {$table}
                (event_id, session_id, user_id, event_type, target_pid, anchor_pid,
                 anchor_term_id, anchor_term_tax, surface, position, ts)
                VALUES " . implode(', ', $placeholders);
        $wpdb->query($wpdb->prepare($sql, $values));
    }

    
    
    
    

    
    public function log_event(array $args) {
        global $wpdb;

        $product_id = isset($args['product_id']) ? (int) $args['product_id'] : 0;
        if ($product_id <= 0) { return false; }

        
        
        
        
        
        
        if ( class_exists('AIEO_Session_Tracker') && method_exists('AIEO_Session_Tracker', 'is_consent_blocking_writes') ) {
            if ( AIEO_Session_Tracker::is_consent_blocking_writes() ) {
                return false;
            }
        }

        $source = isset($args['source']) ? (string) $args['source'] : 'unknown';
        if (!in_array($source, self::VALID_SOURCES, true)) {
            $source = 'unknown';
        }

        $event_id = isset($args['event_id']) ? (string) $args['event_id'] : '';
        if ($event_id === '' || strlen($event_id) > 36) {
            $event_id = $this->generate_uuid_v4();
        }

        $session_id = $this->get_session_id();
        $user_id    = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;

        
        
        
        
        
        
        if ( $session_id !== '' && preg_match('/^[0-9]{1,3}$/', (string) $session_id) ) {
            if ( function_exists('aieo_debug_log') ) {
                aieo_debug_log( '[AIEO_Attribution] Numeric-trivial session_id "' . $session_id . '" — ST cookie may not have initialised yet. product_id=' . $product_id . ' source=' . $source );
            }
        }

        
        
        $cart_position = 0;
        $session_id_clean = sanitize_text_field($session_id);
        if ($session_id_clean !== '') {
            $prev = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM " . self::get_table_name() . "
                 WHERE session_id = %s AND order_id = 0",
                $session_id_clean
            ));
            $cart_position = $prev + 1;
        }

        
        $source_meta = null;
        if (isset($args['source_meta'])) {
            if (is_array($args['source_meta'])) {
                $source_meta = wp_json_encode($args['source_meta']);
            } else {
                
                $decoded = json_decode((string) $args['source_meta'], true);
                if ($decoded !== null || $args['source_meta'] === 'null') {
                    $source_meta = (string) $args['source_meta'];
                } else {
                    $source_meta = wp_json_encode(array('raw' => (string) $args['source_meta']));
                }
            }
            $source_meta = substr((string) $source_meta, 0, 8192); 
        }

        
        
        
        $variation_id_int = isset($args['variation_id']) ? (int) $args['variation_id'] : 0;
        $badge_snapshot   = self::capture_badge_context($product_id, $variation_id_int, $user_id);
        $badge_snapshot_json = wp_json_encode($badge_snapshot);
        if (!is_string($badge_snapshot_json)) { $badge_snapshot_json = null; }

        $ok = $wpdb->insert(
            self::get_table_name(),
            array(
                'event_id'                => $event_id,
                'session_id'              => $session_id_clean,
                'user_id'                 => $user_id,
                'product_id'              => $product_id,
                'variation_id'            => $variation_id_int,
                'quantity'                => isset($args['quantity']) ? max(1, (int) $args['quantity']) : 1,
                'unit_price'              => isset($args['unit_price']) ? (float) $args['unit_price'] : 0.0,
                'source'                  => $source,
                'source_pid'              => isset($args['source_pid']) ? (int) $args['source_pid'] : 0,
                'source_meta'             => $source_meta,
                'badge_context_snapshot'  => $badge_snapshot_json,
                'cart_position'           => $cart_position,
                'ts_added'                => current_time('mysql', true),
                'ga_sent'                 => 0,
            ),
            array('%s', '%s', '%d', '%d', '%d', '%d', '%f', '%s', '%d', '%s', '%s', '%d', '%s', '%d')
        );
        return $ok === false ? false : (int) $wpdb->insert_id;
    }

    
    
    

    
    public function on_order_processed($order_id, $posted_data) {
        $this->backfill_order_id($order_id);
    }

    
    public function on_thankyou($order_id) {
        $this->backfill_order_id((int) $order_id);
    }

    private function backfill_order_id($order_id) {
        if (!$order_id) { return; }
        global $wpdb;

        $order = function_exists('wc_get_order') ? wc_get_order($order_id) : null;
        if (!$order) { return; }

        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        $candidates = array();
        $current    = $this->get_session_id();
        if ($current !== '') { $candidates[] = $current; }
        $cust_id = (int) $order->get_customer_id();
        if ($cust_id > 0) { $candidates[] = (string) $cust_id; }
        $candidates = array_values(array_unique(array_filter($candidates)));

        
        
        $since = gmdate('Y-m-d H:i:s', time() - (7 * DAY_IN_SECONDS));

        $table  = self::get_table_name();
        $order_created_ts = $order->get_date_created() ? $order->get_date_created()->getTimestamp() : time();
        
        $window_since = gmdate('Y-m-d H:i:s', $order_created_ts - (2 * HOUR_IN_SECONDS));
        $window_until = gmdate('Y-m-d H:i:s', $order_created_ts + (10 * MINUTE_IN_SECONDS));

        foreach ($order->get_items() as $item) {
            if (!is_a($item, 'WC_Order_Item_Product')) { continue; }
            $pid = (int) $item->get_product_id();
            if ($pid <= 0) { continue; }

            $matched = 0;

            
            if (!empty($candidates)) {
                $placeholders = implode(',', array_fill(0, count($candidates), '%s'));
                $args = array_merge(
                    array((int) $order_id),
                    $candidates,
                    array($pid, $since)
                );
                
                $matched = (int) $wpdb->query($wpdb->prepare(
                    "UPDATE {$table}
                     SET order_id = %d
                     WHERE session_id IN ({$placeholders})
                       AND product_id = %d
                       AND order_id = 0
                       AND ts_added >= %s",
                    $args
                ));
            }

            if ($matched > 0) { continue; }

            
            
            
            
            
            
            $qty = max(1, (int) $item->get_quantity());
            
            $ids_to_claim = $wpdb->get_col($wpdb->prepare(
                "SELECT id FROM {$table}
                 WHERE product_id = %d
                   AND order_id = 0
                   AND user_id = 0
                   AND session_id LIKE 't\_%%'
                   AND ts_added >= %s AND ts_added <= %s
                 ORDER BY ts_added DESC
                 LIMIT %d",
                $pid, $window_since, $window_until, $qty
            ));
            
            if (!empty($ids_to_claim)) {
                $id_list = implode(',', array_map('intval', $ids_to_claim));
                $wpdb->query($wpdb->prepare(
                    "UPDATE {$table} SET order_id = %d WHERE id IN ({$id_list})",
                    (int) $order_id
                ));
            }
        }
    }

    
    
    

    
    private function get_session_id() {
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        $cookie = 'aieo_attr_sid';
        if (isset($_COOKIE[$cookie]) && (string) $_COOKIE[$cookie] !== '') {
            return sanitize_text_field((string) wp_unslash($_COOKIE[$cookie]));
        }
        $sid = $this->generate_uuid_v4();
        if (!headers_sent()) {
            setcookie($cookie, $sid, time() + (30 * DAY_IN_SECONDS), COOKIEPATH ? COOKIEPATH : '/', COOKIE_DOMAIN);
        }
        $_COOKIE[$cookie] = $sid;
        return $sid;
    }

    private function generate_uuid_v4() {
        
        try {
            $data = random_bytes(16);
            $data[6] = chr(ord($data[6]) & 0x0f | 0x40);
            $data[8] = chr(ord($data[8]) & 0x3f | 0x80);
            return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
        } catch (Exception $e) {
            return uniqid('aieo_', true);
        }
    }

    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

    
    const VALID_CHANNELS = array(
        'web_push',
        'newsletter',
        'paid_google',
        'paid_facebook',
        'paid_microsoft',
        'paid_tiktok',
        'paid_other',
        'organic_search',
        'social',
        'referral',
        'direct',
        'internal',
    );

    
    public static function resolve_channel($landing_page, $referrer) {
        $out = array('channel' => 'direct', 'source' => '', 'campaign' => '', 'medium' => '', 'term' => '', 'content' => '', 'click_id' => '', 'ad_id' => '', 'adset_id' => '', 'ad_campaign_id' => '');

        $landing_page = (string) $landing_page;
        $referrer     = (string) $referrer;

        
        $qs = '';
        if ($landing_page !== '') {
            $parts = wp_parse_url($landing_page);
            if (isset($parts['query'])) { $qs = (string) $parts['query']; }
        }
        $params = array();
        if ($qs !== '') { parse_str($qs, $params); }

        $utm_source   = isset($params['utm_source'])   ? strtolower(trim((string) $params['utm_source']))   : '';
        $utm_medium   = isset($params['utm_medium'])   ? strtolower(trim((string) $params['utm_medium']))   : '';
        $utm_campaign = isset($params['utm_campaign']) ? trim((string) $params['utm_campaign'])             : '';
        $utm_term     = isset($params['utm_term'])     ? trim((string) $params['utm_term'])                 : '';
        $utm_content  = isset($params['utm_content'])  ? trim((string) $params['utm_content'])              : '';
        $aieo_src     = isset($params['aieo_src'])     ? strtolower(trim((string) $params['aieo_src']))     : '';

        
        
        
        $out['source']   = $utm_source;
        $out['campaign'] = $utm_campaign;
        $out['medium']   = $utm_medium;
        $out['term']     = $utm_term;
        $out['content']  = $utm_content;

        
        
        
        
        
        
        
        
        foreach (array('fbclid', 'gclid', 'gbraid', 'wbraid', 'msclkid', 'ttclid', 'twclid', 'li_fat_id', 'epik', 'rdt_cid', 'sc_cid', 'yclid', 'dclid', 'srsltid') as $__cid) {
            if (!empty($params[$__cid])) { $out['click_id'] = (string) $params[$__cid]; break; }
        }
        
        if (isset($params['ad_id']) && $params['ad_id'] !== '') { $out['ad_id'] = (string) $params['ad_id']; }
        
        
        
        if (isset($params['adset_id']) && $params['adset_id'] !== '')         { $out['adset_id'] = (string) $params['adset_id']; }
        elseif (isset($params['adgroup_id']) && $params['adgroup_id'] !== '') { $out['adset_id'] = (string) $params['adgroup_id']; }
        elseif (isset($params['adgroupid']) && $params['adgroupid'] !== '')   { $out['adset_id'] = (string) $params['adgroupid']; }
        
        
        if (isset($params['campaign_id']) && $params['campaign_id'] !== '')   { $out['ad_campaign_id'] = (string) $params['campaign_id']; }
        elseif (isset($params['utm_id']) && $params['utm_id'] !== '')         { $out['ad_campaign_id'] = (string) $params['utm_id']; }

        
        if ($aieo_src === 'push') {
            $out['channel'] = 'web_push';
            if ($out['campaign'] === '' && !empty($params['aieo_dispatch_id'])) {
                $out['campaign'] = 'dispatch_' . (int) $params['aieo_dispatch_id'];
            }
            return $out;
        }
        if ($aieo_src === 'newsletter') {
            $out['channel'] = 'newsletter';
            if ($out['campaign'] === '' && !empty($params['aieo_email_id'])) {
                $out['campaign'] = 'tnp_email_' . (int) $params['aieo_email_id'];
            }
            return $out;
        }

        
        
        if (!empty($params['gclid']) || !empty($params['gbraid']) || !empty($params['wbraid'])) { $out['channel'] = 'paid_google'; if ($out['medium'] === '') $out['medium'] = 'cpc'; return $out; }
        
        
        
        if (!empty($params['fbclid']) || (!empty($params['ad_id']) && !empty($params['adset_id']))) { $out['channel'] = 'paid_facebook';  if ($out['medium'] === '') $out['medium'] = 'cpc'; return $out; }
        if (!empty($params['msclkid'])) { $out['channel'] = 'paid_microsoft'; if ($out['medium'] === '') $out['medium'] = 'cpc'; return $out; }
        if (!empty($params['ttclid']))  { $out['channel'] = 'paid_tiktok';    if ($out['medium'] === '') $out['medium'] = 'paid_social'; return $out; }

        
        if (in_array($utm_medium, array('email', 'newsletter'), true)) {
            $out['channel'] = 'newsletter'; return $out;
        }
        if (in_array($utm_source, array('newsletter', 'tnp', 'mailchimp', 'klaviyo', 'sendgrid', 'sendinblue', 'brevo'), true)) {
            $out['channel'] = 'newsletter'; return $out;
        }
        if (in_array($utm_medium, array('cpc', 'paid', 'ppc', 'paid-social', 'paidsocial'), true)) {
            
            if (in_array($utm_source, array('google', 'adwords', 'google_ads', 'googleads'), true)) {
                $out['channel'] = 'paid_google'; return $out;
            }
            if (in_array($utm_source, array('facebook', 'meta', 'instagram', 'fb'), true)) {
                $out['channel'] = 'paid_facebook'; return $out;
            }
            if (in_array($utm_source, array('tiktok', 'tiktokads', 'tiktok_ads', 'tt'), true)) {
                $out['channel'] = 'paid_tiktok'; return $out;
            }
            $out['channel'] = 'paid_other'; return $out;
        }
        if ($utm_medium === 'social') {
            $out['channel'] = 'social'; return $out;
        }

        
        $ref_host = '';
        if ($referrer !== '') {
            $rp = wp_parse_url($referrer);
            $ref_host = isset($rp['host']) ? strtolower((string) $rp['host']) : '';
        }
        $own_host = '';
        $hp = wp_parse_url((string) home_url('/'));
        if (isset($hp['host'])) { $own_host = strtolower((string) $hp['host']); }

        if ($ref_host === '') {
            $out['channel'] = 'direct'; return $out;
        }
        if ($own_host !== '' && $ref_host === $own_host) {
            
            
            
            
            $out['channel'] = 'direct'; return $out;
        }
        
        $search_hosts = array('google.', 'bing.com', 'duckduckgo.com', 'yahoo.', 'ecosia.org', 'brave.com', 'qwant.com', 'startpage.com', 'yandex.', 'baidu.com');
        foreach ($search_hosts as $h) {
            if (strpos($ref_host, $h) !== false) { $out['channel'] = 'organic_search'; return $out; }
        }
        
        $social_hosts = array('facebook.com', 'fb.com', 'm.facebook.com', 'l.facebook.com', 'instagram.com', 'twitter.com', 'x.com', 't.co', 'tiktok.com', 'pinterest.', 'youtube.com', 'youtu.be', 'reddit.com', 'linkedin.com', 'lnkd.in', 'whatsapp.com', 'telegram.', 'discord.');
        foreach ($social_hosts as $h) {
            if (strpos($ref_host, $h) !== false) { $out['channel'] = 'social'; return $out; }
        }

        
        $out['channel'] = 'referral';
        return $out;
    }

    
    public static function backfill_session_channels($limit = 5000) {
        global $wpdb;
        $tbl = $wpdb->prefix . 'aieo_sessions_statics';

        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT id, landing_page, referrer
             FROM {$tbl}
             WHERE entry_channel = ''
             ORDER BY id ASC
             LIMIT %d",
            (int) $limit
        ), ARRAY_A);
        if (empty($rows)) { return array('processed' => 0, 'by_channel' => array()); }

        $by = array();
        $now = current_time('mysql', true);
        foreach ($rows as $r) {
            $resolved = self::resolve_channel((string) $r['landing_page'], (string) $r['referrer']);
            $campaign_str = mb_substr((string) $resolved['campaign'], 0, 120);
            
            
            
            
            $campaign_id = ($campaign_str !== '' && class_exists('AIEO_DMM_Campaigns'))
                ? AIEO_DMM_Campaigns::touch($campaign_str, (string) $resolved['channel'])
                : null;
            $update_data = array(
                'entry_channel'  => $resolved['channel'],
                'entry_source'   => mb_substr((string) (isset($resolved['source']) ? $resolved['source'] : ''), 0, 80),
                'entry_campaign' => $campaign_str,
                'entry_medium'   => mb_substr((string) $resolved['medium'], 0, 40),
                'entry_term'     => mb_substr((string) $resolved['term'], 0, 80),
                'entry_content'  => mb_substr((string) $resolved['content'], 0, 80),
                'entry_click_id'       => mb_substr((string) (isset($resolved['click_id'])       ? $resolved['click_id']       : ''), 0, 255),
                'entry_ad_id'          => mb_substr((string) (isset($resolved['ad_id'])          ? $resolved['ad_id']          : ''), 0, 40),
                'entry_adset_id'       => mb_substr((string) (isset($resolved['adset_id'])       ? $resolved['adset_id']       : ''), 0, 40),
                'entry_ad_campaign_id' => mb_substr((string) (isset($resolved['ad_campaign_id']) ? $resolved['ad_campaign_id'] : ''), 0, 40),
                'updated_at'     => $now,
            );
            
            
            $update_fmt = array('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s');
            if ($campaign_id) {
                $update_data['entry_campaign_id'] = (int) $campaign_id;
                $update_fmt[]                     = '%d';
            }
            $wpdb->update($tbl, $update_data, array('id' => (int) $r['id']), $update_fmt, array('%d'));
            $ch = $resolved['channel'];
            if (!isset($by[$ch])) { $by[$ch] = 0; }
            $by[$ch]++;
        }
        return array('processed' => count($rows), 'by_channel' => $by);
    }

    
    public static function reconcile_session_from_order($order_id) {
        $order_id = (int) $order_id;
        if ($order_id <= 0 || !function_exists('wc_get_order')) { return false; }
        $order = wc_get_order($order_id);
        if (!$order) { return false; }
        $sid = (string) $order->get_meta('_aieo_attr_sid');
        if ($sid === '') { return false; }

        global $wpdb;
        $tbl = $wpdb->prefix . 'aieo_sessions_statics';
        $row = $wpdb->get_row($wpdb->prepare(
            "SELECT id, entry_channel, order_id FROM {$tbl} WHERE session_id = %s LIMIT 1",
            $sid
        ), ARRAY_A);
        if (!$row) { return false; }

        
        $landing  = (string) $order->get_meta('_wc_order_attribution_session_entry');
        $referrer = (string) $order->get_meta('_wc_order_attribution_referrer');
        
        if ($landing === '') {
            $q = array();
            $g = (string) $order->get_meta('_aieo_gclid');
            if ($g !== '') { $q['gclid'] = $g; }
            foreach (array('utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content') as $u) {
                $v = (string) $order->get_meta('_wc_order_attribution_' . $u);
                if ($v !== '') { $q[$u] = $v; }
            }
            if (!empty($q)) { $landing = home_url('/?' . http_build_query($q)); }
        }

        $resolved = self::resolve_channel($landing, $referrer);
        $new_ch   = (string) $resolved['channel'];

        
        
        $cur          = (string) $row['entry_channel'];
        $do_channel   = in_array($cur, array('', 'internal', 'direct'), true)
                        && !in_array($new_ch, array('', 'internal', 'direct'), true);
        $campaign_str = '';

        $data = array();
        $fmt  = array();
        if ($do_channel) {
            $campaign_str = mb_substr((string) $resolved['campaign'], 0, 120);
            $data['entry_channel']        = $new_ch;                                              $fmt[] = '%s';
            $data['entry_source']         = mb_substr((string) $resolved['source'], 0, 80);        $fmt[] = '%s';
            $data['entry_campaign']       = $campaign_str;                                         $fmt[] = '%s';
            $data['entry_medium']         = mb_substr((string) $resolved['medium'], 0, 40);        $fmt[] = '%s';
            $data['entry_term']           = mb_substr((string) $resolved['term'], 0, 80);          $fmt[] = '%s';
            $data['entry_content']        = mb_substr((string) $resolved['content'], 0, 80);       $fmt[] = '%s';
            $data['entry_click_id']       = mb_substr((string) $resolved['click_id'], 0, 255);     $fmt[] = '%s';
            $data['entry_ad_id']          = mb_substr((string) $resolved['ad_id'], 0, 40);         $fmt[] = '%s';
            $data['entry_adset_id']       = mb_substr((string) $resolved['adset_id'], 0, 40);      $fmt[] = '%s';
            $data['entry_ad_campaign_id'] = mb_substr((string) $resolved['ad_campaign_id'], 0, 40); $fmt[] = '%s';
        }
        
        if ((int) $row['order_id'] <= 0) {
            $data['order_id']    = $order_id;                   $fmt[] = '%d';
            $data['order_total'] = (float) $order->get_total(); $fmt[] = '%f';
        }
        if (empty($data)) { return false; }
        $data['updated_at'] = current_time('mysql', true); $fmt[] = '%s';

        $wpdb->update($tbl, $data, array('id' => (int) $row['id']), $fmt, array('%d'));

        
        if ($do_channel && $campaign_str !== '' && class_exists('AIEO_DMM_Campaigns')) {
            $cid = AIEO_DMM_Campaigns::touch($campaign_str, $new_ch);
            if ($cid) {
                $wpdb->update($tbl, array('entry_campaign_id' => (int) $cid), array('id' => (int) $row['id']), array('%d'), array('%d'));
            }
        }
        return true;
    }
}
