<?php
/**
 * Pixel Injector — Frontend Meta Pixel, event_id sync, deduplication.
 *
 * Injects fbq() base code + event-specific calls.
 * Generates client-side event IDs via crypto.getRandomValues().
 * Thank You page Purchase dedup: PHP-generated event_id passed to JS via inline data.
 *
 * @package CapiFlow
 */

namespace CapiFlow\Core;

defined( 'ABSPATH' ) || exit;

class Pixel_Injector {

    /**
     * Pixel ID cached for noscript fallback (set during enqueue, read during wp_head).
     *
     * @var string
     */
    private $pixel_id = '';

    /**
     * Register hooks.
     */
    public function register_hooks() {
        if ( is_admin() ) {
            return;
        }

        // Enqueue Meta Pixel SDK + fbq init via wp_enqueue_scripts (WP.org compliant).
        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_pixel_base' ), 5 );
        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_tracker_script' ) );
        add_action( 'wp_footer', array( $this, 'inject_page_events' ), 20 );

        // Noscript fallback pixel image (not a <script>, safe for wp_head).
        add_action( 'wp_head', array( $this, 'inject_noscript_pixel' ), 5 );

        // OpenGraph product tags (priority 5: after Yoast at 1, before RankMath at 20).
        add_action( 'wp_head', array( $this, 'inject_og_product_tags' ), 5 );

        // Thank You page Purchase dedup data.
        add_action( 'woocommerce_thankyou', array( $this, 'inject_purchase_dedup_data' ), 5 );

        // Deferred AddToCart: store data in WC session on add-to-cart,
        // so the browser pixel fires on the NEXT page load (after redirect).
        // This ensures Pixel Helper always shows the AddToCart event,
        // matching GTM+Stape.io behavior where GTM4WP fires on redirect target.
        add_action( 'woocommerce_add_to_cart', array( $this, 'store_deferred_addtocart' ), 10, 6 );
    }

    /**
     * Enqueue Meta Pixel SDK (fbevents.js) + fbq init via WordPress enqueue API.
     *
     * Replaces the previous inline <script> approach for WP.org compliance.
     * The fbevents.js URL is filterable by Pro plugin (Ad Blocker proxy).
     */
    public function enqueue_pixel_base() {
        $pixel_id = get_option( 'capiflow_pixel_id', '' );

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

        // GDPR consent check.
        if ( ! $this->has_consent() ) {
            return;
        }

        // Cache for noscript fallback.
        $this->pixel_id = $pixel_id;

        // Build Advanced Matching data for logged-in users (EMQ booster).
        $advanced_matching = $this->get_advanced_matching_data();

        // Generate shared PageView event_id for browser+server dedup.
        $pv_event_id = 'cevt_' . bin2hex( random_bytes( 8 ) ) . '_' . substr( (string) ( microtime( true ) * 1000 ), 0, 13 );

        // ── Enqueue fbevents.js via WordPress API ──────────────────────────
        // Pro plugin filters this URL to serve via first-party proxy (ITP bypass).
        $fbevents_url = apply_filters( 'capiflow_fbevents_script_url', 'https://connect.facebook.net/en_US/fbevents.js' );

        // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- External CDN script, version managed by Meta.
        wp_enqueue_script( 'capiflow-fbevents', $fbevents_url, array(), null, false );

        // ── fbq() stub + initialization (before fbevents.js loads) ─────────
        // Stub creates the fbq queue function so fbq() calls are buffered
        // until the SDK script finishes loading. Script loading is handled
        // by wp_enqueue_script above, so we omit the createElement portion
        // of the original Meta snippet.
        $inline_js  = '!function(f){if(f.fbq)return;var n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version="2.0";n.queue=[]}(window);';
        $inline_js .= 'fbq("init","' . esc_js( $pixel_id ) . '",' . wp_json_encode( $advanced_matching ) . ');';
        $inline_js .= 'fbq("track","PageView",{},{eventID:"' . esc_js( $pv_event_id ) . '"});';
        $inline_js .= 'window.capiflowPageViewId="' . esc_js( $pv_event_id ) . '";';

        wp_add_inline_script( 'capiflow-fbevents', $inline_js, 'before' );
    }

    /**
     * Output noscript fallback pixel (tracking image for JS-disabled browsers).
     *
     * Not a <script> tag — just an <img> inside <noscript>. Safe for wp_head.
     * Only outputs if enqueue_pixel_base() ran successfully (pixel_id is set).
     */
    public function inject_noscript_pixel() {
        if ( empty( $this->pixel_id ) ) {
            return;
        }

        $noscript_url = apply_filters( 'capiflow_noscript_pixel_url', 'https://www.facebook.com/tr' );
        ?>
<!-- CapiFlow — Noscript Pixel Fallback -->
<noscript><img height="1" width="1" style="display:none"
src="<?php echo esc_url( $noscript_url ); ?>?id=<?php echo esc_attr( $this->pixel_id ); ?>&ev=PageView&noscript=1"
/></noscript>
        <?php
    }

    /**
     * Get Advanced Matching data for fbq('init').
     *
     * CRITICAL: Values must be UNHASHED and only normalized!
     * Meta Pixel SDK automatically applies SHA-256 hashing.
     * If we pre-hash, the SDK will double-hash → no match → EMQ = 0!
     *
     * For CAPI server-side, WE hash manually (different code path).
     * Both sides use the SAME base value → Meta matches them.
     *
     * @see https://developers.facebook.com/docs/facebook-pixel/advanced/advanced-matching/
     * @return array Normalized (UNHASHED) user data for Advanced Matching.
     */
    private function get_advanced_matching_data() {
        $am = array();

        // ── External ID for ALL users (logged-in + guest) ──
        if ( is_user_logged_in() ) {
            // Logged-in: use WP user ID (matches server-side external_id).
            $am['external_id'] = 'wp_' . get_current_user_id();
        } else {
            // Guest: use cs_uid cookie (matches server-side Cookie_Manager).
            if ( ! empty( $_COOKIE['cs_uid'] ) ) {
                $am['external_id'] = sanitize_text_field( wp_unslash( $_COOKIE['cs_uid'] ) );
            }
        }

        // ── PII only for logged-in users ──
        if ( ! is_user_logged_in() ) {
            return $am;
        }

        $user_id = get_current_user_id();

        // Try WooCommerce customer data first (has billing info).
        if ( class_exists( 'WC_Customer' ) ) {
            try {
                $customer = new \WC_Customer( $user_id );

                // Email — MOST important for matching.
                $email = $customer->get_billing_email();
                if ( empty( $email ) ) {
                    $email = $customer->get_email();
                }
                if ( ! empty( $email ) ) {
                    $am['em'] = strtolower( trim( $email ) );
                }

                // Phone — E.164 normalized (country code + digits only).
                $phone = $customer->get_billing_phone();
                if ( ! empty( $phone ) ) {
                    $normalized = Data_Normalizer::normalize_phone_e164( $phone, $customer->get_billing_country() );
                    if ( ! empty( $normalized ) ) {
                        $am['ph'] = $normalized;
                    }
                }

                // First Name — Meta normalized (lowercase, strip punctuation, Unicode-safe).
                $fn = $customer->get_billing_first_name();
                if ( ! empty( $fn ) ) {
                    $normalized_fn = Data_Normalizer::normalize_name( $fn );
                    if ( ! empty( $normalized_fn ) ) {
                        $am['fn'] = $normalized_fn;
                    }
                }

                // Last Name — Meta normalized.
                $ln = $customer->get_billing_last_name();
                if ( ! empty( $ln ) ) {
                    $normalized_ln = Data_Normalizer::normalize_name( $ln );
                    if ( ! empty( $normalized_ln ) ) {
                        $am['ln'] = $normalized_ln;
                    }
                }

                // City — Meta normalized (Unicode-safe, no punctuation/spaces).
                $ct = $customer->get_billing_city();
                if ( ! empty( $ct ) ) {
                    $normalized_ct = Data_Normalizer::normalize_city( $ct );
                    if ( ! empty( $normalized_ct ) ) {
                        $am['ct'] = $normalized_ct;
                    }
                }

                // State — Meta normalized (strip punctuation, lowercase).
                $st = $customer->get_billing_state();
                if ( ! empty( $st ) ) {
                    $normalized_st = Data_Normalizer::normalize_state( $st );
                    if ( ! empty( $normalized_st ) ) {
                        $am['st'] = $normalized_st;
                    }
                }

                // Zip — Meta normalized (US 5+4 handling, strip spaces/dashes).
                $zp = $customer->get_billing_postcode();
                if ( ! empty( $zp ) ) {
                    $normalized_zp = Data_Normalizer::normalize_zip( $zp );
                    if ( ! empty( $normalized_zp ) ) {
                        $am['zp'] = $normalized_zp;
                    }
                }

                // Country — Meta normalized (fast-path for 2-letter ISO).
                $co = $customer->get_billing_country();
                if ( ! empty( $co ) ) {
                    $normalized_co = Data_Normalizer::normalize_country( $co );
                    if ( ! empty( $normalized_co ) ) {
                        $am['country'] = $normalized_co;
                    }
                }
            } catch ( \Exception $e ) {
                // Fall through to WP user fallback.
            }
        }

        // Fallback: WP user data (if no WooCommerce customer).
        if ( ! isset( $am['em'] ) ) {
            $user = get_userdata( $user_id );
            if ( $user ) {
                if ( ! empty( $user->user_email ) ) {
                    $am['em'] = strtolower( trim( $user->user_email ) );
                }
                if ( ! empty( $user->first_name ) ) {
                    $normalized_fn = Data_Normalizer::normalize_name( $user->first_name );
                    if ( ! empty( $normalized_fn ) ) {
                        $am['fn'] = $normalized_fn;
                    }
                }
                if ( ! empty( $user->last_name ) ) {
                    $normalized_ln = Data_Normalizer::normalize_name( $user->last_name );
                    if ( ! empty( $normalized_ln ) ) {
                        $am['ln'] = $normalized_ln;
                    }
                }
            }
        }

        return $am;
    }

    /**
     * Enqueue the main tracker JavaScript.
     */
    public function enqueue_tracker_script() {
        $pixel_id = get_option( 'capiflow_pixel_id', '' );
        if ( empty( $pixel_id ) ) {
            return;
        }

        if ( ! $this->has_consent() ) {
            return;
        }

        $tracker_file = CAPIFLOW_PLUGIN_DIR . 'assets/js/pixel-tracker.js';
        $tracker_ver  = CAPIFLOW_VERSION . '.' . ( file_exists( $tracker_file ) ? filemtime( $tracker_file ) : '' );

        wp_enqueue_script(
            'capiflow-tracker',
            CAPIFLOW_PLUGIN_URL . 'assets/js/pixel-tracker.js',
            array(),
            $tracker_ver,
            true
        );

        // WP 6.3+ defer support.
        if ( function_exists( 'wp_script_add_data' ) && version_compare( get_bloginfo( 'version' ), '6.3', '>=' ) ) {
            wp_script_add_data( 'capiflow-tracker', 'strategy', 'defer' );
        } else {
            add_filter( 'script_loader_tag', array( $this, 'add_defer_attribute' ), 10, 2 );
        }

        // Localize tracker config.
        wp_localize_script( 'capiflow-tracker', 'capiflowConfig', array(
            'proxyUrl'       => apply_filters( 'capiflow_proxy_url', rest_url( 'capiflow/v1/track' ) ),
            'pixelId'        => $pixel_id,
            'currency'       => function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'BDT',
            'eventsConfig'   => array(
                'pageview'         => (bool) get_option( 'capiflow_send_pageview', true ),
                'viewcontent'      => (bool) get_option( 'capiflow_send_viewcontent', true ),
                'search'           => (bool) get_option( 'capiflow_send_search', true ),
                'addtocart'        => (bool) get_option( 'capiflow_send_addtocart', true ),
                'initiatecheckout' => (bool) get_option( 'capiflow_send_initiatecheckout', true ),
                'addpaymentinfo'   => (bool) get_option( 'capiflow_send_addpaymentinfo', true ),
            ),
            // Pre-hashed server PII for logged-in users.
            // is_user_logged_in() is TRUE here (page render) but FALSE in REST context.
            // JS passes this to proxy so CAPI server events carry full PII.
            'serverUserData' => $this->get_server_user_data(),
            // product_id → SKU map so JS AddToCart uses same ID as PHP ViewContent.
            'skuMap'         => $this->get_product_sku_map(),
        ) );
    }

    /**
     * Inject page-context-specific event data in footer.
     */
    public function inject_page_events() {
        $pixel_id = get_option( 'capiflow_pixel_id', '' );
        if ( empty( $pixel_id ) || ! $this->has_consent() ) {
            return;
        }

        // ── Deferred AddToCart (separate from page-context events) ──
        // When a single-product form submit redirects the page, the JS fbq()
        // request from the original page gets cancelled by the browser.
        // Solution: fire AddToCart on the NEXT page (redirect target) using
        // data stored in WC session. Pixel Helper then sees the event.
        // Uses its OWN script tag to avoid overwriting ViewContent/Search events.
        $deferred_atc = $this->consume_deferred_addtocart();
        if ( ! empty( $deferred_atc ) ) {
            echo '<script id="capiflow-deferred-atc" type="application/json">';
            echo wp_json_encode( $deferred_atc );
            echo '</script>';
        }

        $event_data = array();

        // ViewContent — Single product page.
        if ( function_exists( 'is_product' ) && is_product() ) {
            global $product;
            if ( $product instanceof \WC_Product ) {
                $id = Event_Builder::resolve_content_id( $product );

                // Category: use parent ID for variations (variations inherit parent categories).
                $cat_post_id = ( $product instanceof \WC_Product_Variation )
                    ? $product->get_parent_id()
                    : $product->get_id();
                $cats = wp_get_post_terms( $cat_post_id, 'product_cat', array( 'fields' => 'names' ) );

                // content_type: ViewContent already has correct dynamic logic — keep as-is.
                $content_type = $product->is_type( 'variable' ) ? 'product_group' : 'product';

                $event_data = array(
                    'event'       => 'ViewContent',
                    'custom_data' => array(
                        'content_ids'      => array( $id ),
                        'content_type'     => $content_type,
                        'content_name'     => $product->get_name(),
                        'content_category' => ( is_array( $cats ) && ! is_wp_error( $cats ) && ! empty( $cats ) ) ? $cats[0] : '',
                        'value'            => (float) $product->get_price(),
                        'currency'         => get_woocommerce_currency(),
                        'contents'         => array( array( 'id' => $id, 'quantity' => 1 ) ),
                    ),
                );
            }
        }

        // Search event.
        if ( function_exists( 'is_search' ) && is_search() ) {
            $search_ids   = array();
            $search_items = array();

            // Add content_ids from search results (max 10 — Meta limit).
            global $wp_query;
            if ( $wp_query && ! empty( $wp_query->posts ) ) {
                $count = 0;
                foreach ( $wp_query->posts as $post ) {
                    if ( $count >= 10 ) {
                        break;
                    }
                    $search_product = wc_get_product( $post->ID );
                    if ( ! $search_product ) {
                        continue;
                    }
                    $search_id      = Event_Builder::resolve_content_id( $search_product );
                    $search_ids[]   = $search_id;
                    $search_items[] = array( 'id' => $search_id, 'quantity' => 1 );
                    $count++;
                }
            }

            $event_data = array(
                'event'       => 'Search',
                'custom_data' => array(
                    'search_string' => get_search_query(),
                    'content_type'  => 'product',
                    'content_ids'   => $search_ids,
                    'contents'      => $search_items,
                ),
            );
        }

        // InitiateCheckout — Checkout page.
        if ( function_exists( 'is_checkout' ) && is_checkout() && ! is_order_received_page() ) {
            $cart = WC()->cart;
            if ( $cart && ! $cart->is_empty() ) {
                $cart_ids   = array();
                $cart_items = array();

                foreach ( $cart->get_cart() as $item ) {
                    $capiflow_product = isset( $item['data'] ) ? $item['data'] : null;
                    if ( ! $capiflow_product instanceof \WC_Product ) {
                        continue;
                    }

                    $id = Event_Builder::resolve_content_id( $capiflow_product );

                    $cart_ids[]   = $id;
                    $cart_items[] = array(
                        'id'         => $id,
                        'quantity'   => $item['quantity'],
                        'item_price' => Event_Builder::resolve_item_price( $capiflow_product ),
                    );
                }

                $event_data = array(
                    'event'       => 'InitiateCheckout',
                    'custom_data' => array(
                        'value'        => (float) $cart->get_cart_contents_total(),
                        'currency'     => get_woocommerce_currency(),
                        'content_type' => Event_Builder::resolve_content_type(),
                        'content_ids'  => $cart_ids,
                        'contents'     => $cart_items,
                        'num_items'    => $cart->get_cart_contents_count(),
                    ),
                );
            }
        }

        if ( ! empty( $event_data ) ) {
            echo '<script id="capiflow-page-data" type="application/json">';
            echo wp_json_encode( $event_data );
            echo '</script>';
        }
    }

    /**
     * Inject Purchase dedup data on Thank You page.
     *
     * The server generates the event_id. JS fires fbq('track','Purchase')
     * with the SAME event_id → Meta deduplicates browser + server events.
     *
     * @param int $order_id Order ID.
     */
    public function inject_purchase_dedup_data( $order_id ) {
        $order = wc_get_order( $order_id );
        if ( ! $order ) {
            return;
        }

        // ── COD Gate: Block browser Purchase for COD orders ──────────────
        // Meta official: "Only fire Purchase when definitively finalized."
        // COD orders are not finalized at Thank You page — admin must confirm.
        // Server-side CAPI fires later when admin confirms (with full PII from order meta).
        // PRO: overrides filter → fires immediately + sends Negative on cancel.
        $cod_gate = (bool) apply_filters( 'capiflow_cod_gate_enabled', true );

        if ( $cod_gate && 'cod' === $order->get_payment_method() ) {
            return; // No browser pixel for COD — server-only event on confirm.
        }

        // Prevent double-fire on page refresh.
        $already_fired = $order->get_meta( '_capiflow_pixel_fired', true );
        if ( ! empty( $already_fired ) ) {
            // Still output dedup data for the browser pixel (same event_id).
            $event_id = $order->get_meta( '_capiflow_purchase_event_id', true );
            if ( ! empty( $event_id ) ) {
                $this->output_purchase_pixel_data( $order, $event_id );
            }
            return;
        }

        // Get the server-generated event_id (set by Purchase Controller).
        $event_id = $order->get_meta( '_capiflow_purchase_event_id', true );
        if ( empty( $event_id ) ) {
            // Generate one if Purchase Controller hasn't fired yet.
            $event_id = Event_ID_Generator::generate();
            $order->update_meta_data( '_capiflow_purchase_event_id', $event_id );
            $order->save_meta_data();
        }

        $this->output_purchase_pixel_data( $order, $event_id );

        // Mark as fired (prevents TY page refresh double-fire).
        $order->update_meta_data( '_capiflow_pixel_fired', '1' );
        $order->save_meta_data();
    }

    /**
     * Output Purchase pixel data as inline JSON for the tracker JS.
     *
     * @param \WC_Order $order    Order.
     * @param string    $event_id Shared event ID.
     */
    private function output_purchase_pixel_data( $order, $event_id ) {
        $content_ids = array();
        $contents    = array();
        $total_qty   = 0;

        foreach ( $order->get_items() as $item ) {
            if ( ! $item instanceof \WC_Order_Item_Product ) {
                continue;
            }
            $product = $item->get_product();
            if ( ! $product ) {
                continue;
            }

            $id = Event_Builder::resolve_content_id( $product );

            $content_ids[] = $id;
            $total_qty    += $item->get_quantity();

            $contents[] = array(
                'id'         => $id,
                'quantity'   => $item->get_quantity(),
                'item_price' => Event_Builder::resolve_item_price( $item ),
            );
        }

        // Content name — matches Event_Builder logic (multi-item = concatenated).
        $items      = array_values( $order->get_items() );
        $item_count = count( $items );
        $content_name = '';
        $content_cat  = '';

        if ( $item_count > 0 ) {
            // First item name + category.
            $content_name = $items[0]->get_name();
            $first_product = $items[0]->get_product();
            if ( $first_product ) {
                // Fix: use parent ID for variations — variations have no category assignment.
                $cat_post_id = ( $first_product instanceof \WC_Product_Variation )
                    ? $first_product->get_parent_id()
                    : $first_product->get_id();
                $cats = wp_get_post_terms( $cat_post_id, 'product_cat', array( 'fields' => 'names' ) );
                $content_cat = ( is_array( $cats ) && ! is_wp_error( $cats ) && ! empty( $cats ) ) ? $cats[0] : '';
            }

            // Multi-item: concatenate names (max 5).
            if ( $item_count > 1 ) {
                $names = array();
                foreach ( $items as $item ) {
                    $names[] = $item->get_name();
                }
                $content_name = implode( ', ', array_slice( $names, 0, 5 ) );
                if ( $item_count > 5 ) {
                    $content_name .= ' +' . ( $item_count - 5 ) . ' more';
                }
            }
        }

        $purchase_data = array(
            'event'    => 'Purchase',
            'eventId'  => $event_id,
            'custom_data' => array(
                'value'            => (float) $order->get_total(),
                'currency'         => $order->get_currency(),
                'content_ids'      => $content_ids,
                'content_type'     => Event_Builder::resolve_content_type(),
                'content_name'     => $content_name,
                'content_category' => $content_cat,
                'contents'         => $contents,
                'num_items'        => $total_qty,
                'order_id'         => $order->get_order_number(),
            ),
        );

        echo '<script id="capiflow-purchase-data" type="application/json">';
        echo wp_json_encode( $purchase_data );
        echo '</script>';
    }

    /**
     * Store AddToCart event data in WC session for deferred firing.
     *
     * When a single-product form submit triggers a page redirect, the JS
     * fbq('track','AddToCart') request on the original page gets cancelled
     * by the browser. By storing the event data in the WC session, we can
     * fire it on the NEXT page load (redirect target). This ensures Pixel
     * Helper always shows the AddToCart event — matching GTM+Stape.io behavior.
     *
     * Hooked on: woocommerce_add_to_cart (priority 10).
     *
     * @param string $cart_item_key Cart item hash.
     * @param int    $product_id    Product ID.
     * @param int    $quantity      Quantity.
     * @param int    $variation_id  Variation ID (0 if simple).
     * @param array  $variation     Variation attributes.
     * @param array  $cart_item_data Extra cart item data.
     */
    public function store_deferred_addtocart( $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data ) {
        if ( ! function_exists( 'WC' ) || ! WC()->session ) {
            return;
        }

        // Only for non-AJAX requests (single product form submit → redirect).
        // AJAX add-to-cart (archive pages) doesn't navigate, so the JS
        // click handler fires fbq() without issue.
        if ( wp_doing_ajax() ) {
            return;
        }

        // Determine actual product (variation or parent).
        $actual_id = $variation_id ? $variation_id : $product_id;
        $product   = wc_get_product( $actual_id );
        if ( ! $product ) {
            return;
        }

        $id = Event_Builder::resolve_content_id( $product );
        $event_id = isset( $_COOKIE['capiflow_atc_event_id'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['capiflow_atc_event_id'] ) ) : '';

        $event_data = array(
            'event'        => 'AddToCart',
            'eventId'      => $event_id,
            // The CAPI server event was already sent via sendBeacon on the
            // original page (survives navigation). This deferred event only
            // needs to fire the browser pixel (fbq) so Pixel Helper sees it.
            'browser_only' => true,
            'custom_data'  => array(
                'content_ids'  => array( $id ),
                'content_type' => Event_Builder::resolve_content_type( $product ),
                'content_name' => $product->get_name(),
                'value'        => (float) $product->get_price() * $quantity,
                'currency'     => get_woocommerce_currency(),
                'contents'     => array( array(
                    'id'         => $id,
                    'quantity'   => $quantity,
                    'item_price' => Event_Builder::resolve_item_price( $product ),
                ) ),
            ),
        );

        // Each deferred event gets a unique nonce — prevents reload re-fire
        // without blocking future legitimate add-to-cart events.
        $nonce = wp_generate_password( 12, false );
        $event_data['_nonce']     = $nonce;
        $event_data['_timestamp'] = time();

        WC()->session->set( 'capiflow_deferred_atc', $event_data );
    }

    /**
     * Consume deferred AddToCart data from WC session.
     *
     * Uses a unique nonce per event to prevent reload re-fire while
     * never blocking legitimate new add-to-cart actions.
     * Also rejects data older than 60 seconds (stale safety net).
     *
     * @return array|null Event data array, or null if none pending.
     */
    private function consume_deferred_addtocart() {
        if ( ! function_exists( 'WC' ) || ! WC()->session ) {
            return null;
        }

        $data = WC()->session->get( 'capiflow_deferred_atc' );
        if ( empty( $data ) || ! is_array( $data ) ) {
            return null;
        }

        // Stale data guard — reject if older than 60 seconds.
        if ( isset( $data['_timestamp'] ) && ( time() - $data['_timestamp'] ) > 60 ) {
            WC()->session->__unset( 'capiflow_deferred_atc' );
            return null;
        }

        // Nonce dedup — check if THIS specific event was already consumed.
        $nonce          = $data['_nonce'] ?? '';
        $consumed_nonce = WC()->session->get( 'capiflow_atc_consumed_nonce' );
        if ( $nonce && $nonce === $consumed_nonce ) {
            return null; // Same event already fired — page reload.
        }

        // Mark THIS nonce as consumed (new ATCs generate different nonces).
        if ( $nonce ) {
            WC()->session->set( 'capiflow_atc_consumed_nonce', $nonce );
        }

        // Also try to clear the main data.
        WC()->session->__unset( 'capiflow_deferred_atc' );

        // Strip internal fields before returning to JS.
        unset( $data['_nonce'], $data['_timestamp'] );

        return $data;
    }

    /**
     * Add defer attribute for pre-6.3 WordPress.
     *
     * @param string $tag    Script tag HTML.
     * @param string $handle Script handle.
     * @return string Modified tag.
     */
    public function add_defer_attribute( $tag, $handle ) {
        if ( 'capiflow-tracker' !== $handle ) {
            return $tag;
        }

        if ( strpos( $tag, 'defer' ) !== false ) {
            return $tag;
        }

        return str_replace( ' src', ' defer src', $tag );
    }

    /**
     * Check GDPR/consent (filterable).
     *
     * @return bool True if consent given.
     */
    private function has_consent() {
        // Option A: toggle OFF = always allow browser pixel.
        if ( ! get_option( 'capiflow_consent_enforcement', false ) ) {
            return true;
        }
        return apply_filters( 'capiflow_has_tracking_consent', true );
    }

    /**
     * Build pre-hashed server PII for logged-in users.
     *
     * Called during PHP page render (is_user_logged_in() is TRUE).
     * Embedded in capiflowConfig so JS can pass it to the REST proxy,
     * which runs in a context where is_user_logged_in() returns FALSE.
     *
     * @return array Hashed user_data fields (em, ph, fn, ln, ct, st, zp, country).
     */
    private function get_server_user_data() {
        if ( ! is_user_logged_in() ) {
            return array();
        }

        $user_id  = get_current_user_id();
        $customer = null;

        if ( class_exists( 'WC_Customer' ) ) {
            try {
                $customer = new \WC_Customer( $user_id );
            } catch ( \Exception $e ) {
                $customer = null;
            }
        }

        $data = array();

        // ── External ID — pre-hashed for REST proxy (fixes auth scope mismatch) ──
        // Browser AM sends raw 'wp_{id}' → SDK auto-hashes → sha256('wp_{id}').
        // REST proxy can't detect logged-in (no nonce) → would use cs_uid instead.
        // Pre-hashing here and passing via serverUserData ensures browser ↔ server match.
        $data['external_id'] = array( hash( 'sha256', 'wp_' . $user_id ) );

        if ( $customer ) {
            $email = $customer->get_billing_email() ?: $customer->get_email();
            $em    = Data_Normalizer::hash_email( $email );
            if ( $em !== null ) {
                $data['em'] = array( $em );
            }

            $ph = Data_Normalizer::hash_phone( $customer->get_billing_phone(), $customer->get_billing_country() );
            if ( $ph !== null ) {
                $data['ph'] = array( $ph );
            }

            $fn = Data_Normalizer::hash_name( $customer->get_billing_first_name() );
            if ( $fn !== null ) {
                $data['fn'] = array( $fn );
            }

            $ln = Data_Normalizer::hash_name( $customer->get_billing_last_name() );
            if ( $ln !== null ) {
                $data['ln'] = array( $ln );
            }

            $ct = Data_Normalizer::hash_city( $customer->get_billing_city() );
            if ( $ct !== null ) {
                $data['ct'] = array( $ct );
            }

            $st = Data_Normalizer::hash_state( $customer->get_billing_state() );
            if ( $st !== null ) {
                $data['st'] = array( $st );
            }

            $zp = Data_Normalizer::hash_zip( $customer->get_billing_postcode() );
            if ( $zp !== null ) {
                $data['zp'] = array( $zp );
            }

            $co = Data_Normalizer::hash_country( $customer->get_billing_country() );
            if ( $co !== null ) {
                $data['country'] = array( $co );
            }
        } else {
            // WP user fallback.
            $user = get_userdata( $user_id );
            if ( $user && ! empty( $user->user_email ) ) {
                $em = Data_Normalizer::hash_email( $user->user_email );
                if ( $em !== null ) {
                    $data['em'] = array( $em );
                }
            }
        }

        return $data;
    }

    /**
     * Build a product_id → SKU map for products on the current page.
     *
     * Used by JS AddToCart to use consistent content_ids (SKU when available,
     * product ID as fallback) — matching PHP ViewContent/InitiateCheckout logic.
     *
     * @return array { product_id: sku_or_id } map.
     */
    private function get_product_sku_map() {
        $map = array();

        // Single product page.
        if ( function_exists( 'is_product' ) && is_product() ) {
            global $product;
            if ( $product instanceof \WC_Product ) {
                $id = $product->get_id();
                $map[ $id ] = Event_Builder::resolve_content_id( $product );

                // Also map variation IDs.
                if ( $product->is_type( 'variable' ) ) {
                    foreach ( $product->get_children() as $variation_id ) {
                        $variation = wc_get_product( $variation_id );
                        if ( $variation ) {
                            $map[ $variation_id ] = Event_Builder::resolve_content_id( $variation );
                        }
                    }
                }
            }
            return $map;
        }

        // Shop / category page — loop through current query.
        global $wp_query;
        if ( $wp_query && ! empty( $wp_query->posts ) ) {
            foreach ( $wp_query->posts as $post ) {
                $product = wc_get_product( $post->ID );
                if ( ! $product ) {
                    continue;
                }
                $map[ $post->ID ] = Event_Builder::resolve_content_id( $product );
            }
        }

        return $map;
    }
    /**
     * Inject Open Graph product tags for catalog matching.
     *
     * Embeds product:retailer_item_id with the same content_id used in CAPI
     * events so Facebook/Instagram catalog matching works without a GTM data layer.
     * Respects capiflow_enable_og_product_tags setting (default OFF).
     * Yoast WooCommerce SEO is auto-detected to avoid duplicate price tags.
     *
     * Priority 5 on wp_head: after Yoast (1), before RankMath (20).
     */
    public function inject_og_product_tags(): void {
        // Feature must be explicitly enabled.
        if ( ! get_option( 'capiflow_enable_og_product_tags', false ) ) {
            return;
        }

        if ( ! function_exists( 'is_product' ) || ! is_product() ) {
            return;
        }

        // Resolve product object.
        global $product;
        if ( ! $product instanceof \WC_Product ) {
            $product = function_exists( 'wc_get_product' ) ? wc_get_product( get_the_ID() ) : null;
        }
        if ( ! $product instanceof \WC_Product ) {
            return;
        }

        // Resolved content_id (respects capiflow_content_id_strategy).
        $content_id = Event_Builder::resolve_content_id( $product );

        echo '<meta property="product:retailer_item_id" content="' . esc_attr( $content_id ) . '">' . "\n";
        echo '<meta property="product:availability" content="' . esc_attr( $product->is_in_stock() ? 'in stock' : 'out of stock' ) . '">' . "\n";

        // Price: only output if Yoast WooCommerce SEO is NOT already handling it.
        // Yoast WooCommerce SEO hooks into wpseo_opengraph to add product prices.
        $yoast_woo_active = class_exists( 'Yoast\WP\Woocommerce\Yoast_WC_SEO' )
            || has_action( 'wpseo_opengraph' );

        if ( ! $yoast_woo_active ) {
            echo '<meta property="product:price:amount" content="' . esc_attr( (float) $product->get_price() ) . '">' . "\n";
            echo '<meta property="product:price:currency" content="' . esc_attr( get_woocommerce_currency() ) . '">' . "\n";
        }
    }
}
