/**
 * Opti-Behavior Simple Heatmap Tracker
 * A lightweight replacement for the broken minified heatmap script
 * 
 * This script tracks click events and sends them to the server for heatmap visualization
 */

(function() {
    'use strict';

    // -------------------------------------------------------------------------
    // Nonce refresh — one refresh per page load to handle cached-page expiry
    // -------------------------------------------------------------------------
    var _nonceRefreshed = false;

    /**
     * Request fresh nonces from server, update the tracker config, and re-queue
     * any events that failed due to the stale nonce.
     *
     * @param {Object}   trackerConfig   The HeatmapTracker instance's config object.
     * @param {Array}    eventsToRequeue Events to put back into the tracker's queue.
     * @param {Object}   trackerInstance The HeatmapTracker instance.
     */
    function refreshNonceAndRequeue( trackerConfig, eventsToRequeue, trackerInstance ) {
        if ( _nonceRefreshed ) { return; }
        _nonceRefreshed = true;

        var xhr = new XMLHttpRequest();
        xhr.open( 'POST', trackerConfig.ajax_url );
        xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
        xhr.onload = function() {
            if ( xhr.status === 200 ) {
                try {
                    var resp = JSON.parse( xhr.responseText );
                    if ( resp.success && resp.data && resp.data.nonce_heatmap ) {
                        trackerConfig.nonce = resp.data.nonce_heatmap;
                        // Re-add the unsent events to the front of the queue
                        // so the next periodic timer interval re-sends them.
                        if ( eventsToRequeue && eventsToRequeue.length > 0 ) {
                            Array.prototype.unshift.apply( trackerInstance.eventQueue, eventsToRequeue );
                        }
                        if ( window.OptiBehaviorDebug ) {
                            window.OptiBehaviorDebug.debug( 'Nonce refreshed; re-queued ' + ( eventsToRequeue ? eventsToRequeue.length : 0 ) + ' events', 'heatmap-simple' );
                        }
                    }
                } catch ( e ) {
                    if ( window.OptiBehaviorDebug ) {
                        window.OptiBehaviorDebug.error( 'Nonce refresh parse error', 'heatmap-simple', e );
                    }
                }
            }
        };
        xhr.send( 'action=opti_behavior_refresh_nonces' );
    }

    // Wait for config variable to be available before dispatching init.
    // JS combine plugins (LiteSpeed Cache, WP Rocket, Autoptimize) may displace
    // the wp_localize_script inline data into a combined bundle that loads AFTER
    // this script. Poll up to 10 seconds to handle that displacement scenario;
    // if config is already present (normal case) initialization is immediate.
    (function() {
        var _waited = 0;
        function dispatch() {
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', init);
            } else {
                init();
            }
        }
        function check() {
            if (typeof window.opti_behavior_heatmap !== 'undefined') {
                dispatch();
            } else if (_waited >= 10000) {
                // Config never appeared — optimization plugin may have stripped it
                if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.error('opti_behavior_heatmap config not found after 10s', 'heatmap-simple');
            } else {
                _waited += 50;
                setTimeout(check, 50);
            }
        }
        check();
    }());
    
    function init() {
        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('init() called', 'heatmap-simple');

        // Check if we have the required configuration
        if (typeof window.opti_behavior_heatmap === 'undefined') {
            if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.error('Configuration not found', 'heatmap-simple');
            return;
        }

        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Config found:', 'heatmap-simple', window.opti_behavior_heatmap);

        const config = window.opti_behavior_heatmap;

        // Only initialize in reporter mode
        if (config._mode !== 'reporter') {
            if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Not in reporter mode, mode is:', 'heatmap-simple', config._mode);
            return;
        }

        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Reporter mode confirmed', 'heatmap-simple');

        // Check if click tracking is enabled
        const reports = Array.isArray(config.reports) ? config.reports : config.reports.split(',');
        const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
        const clickEvent = isMobile ? 'click_mobile' : 'click_pc';

        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Reports:', 'heatmap-simple', reports);
        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('isMobile:', 'heatmap-simple', isMobile);
        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Looking for event:', 'heatmap-simple', clickEvent);
        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Event included?', 'heatmap-simple', reports.includes(clickEvent));

        if (!reports.includes(clickEvent)) {
            if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Click tracking not enabled for this device type', 'heatmap-simple');
            return;
        }

        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Initializing tracker...', 'heatmap-simple');

        /**
         * Start the tracker, optionally overriding privacy_mode to anonymous
         * when full-mode consent has been rejected by the visitor.
         *
         * @param {string} consentStatus 'granted' | 'rejected' | 'anonymous'
         */
        function startTracker( consentStatus ) {
            if ( consentStatus === 'rejected' ) {
                // Visitor declined full tracking — fall back to anonymous behaviour.
                config.privacy_mode = 'anonymous';
                if ( window.optiBehaviorHeatmapConfig ) {
                    window.optiBehaviorHeatmapConfig.privacy_mode = 'anonymous';
                }
                if ( window.OptiBehaviorDebug ) {
                    window.OptiBehaviorDebug.debug( 'Consent rejected — switching to anonymous mode', 'heatmap-simple' );
                }
            }
            var tracker = new HeatmapTracker( config );
            tracker.start();
            if ( window.OptiBehaviorDebug ) {
                window.OptiBehaviorDebug.debug( 'Tracker initialized (consent: ' + consentStatus + ')', 'heatmap-simple', config );
            }
        }

        var privacyMode = config.privacy_mode || 'anonymous';

        /**
         * Read the raw optibehavior_consent cookie value.
         * Used as a fallback when window.optiBehaviorConsentState is not yet set
         * (e.g. the consent JS hasn't executed before this script on a reload).
         *
         * @return {string|null} 'granted', 'rejected', or null if cookie absent.
         */
        function getConsentCookie() {
            var nameEQ = 'optibehavior_consent=';
            var parts  = document.cookie.split( ';' );
            for ( var i = 0; i < parts.length; i++ ) {
                var part = parts[ i ];
                while ( part.charAt( 0 ) === ' ' ) { part = part.substring( 1 ); }
                if ( part.indexOf( nameEQ ) === 0 ) {
                    return decodeURIComponent( part.substring( nameEQ.length ) );
                }
            }
            return null;
        }

        // Resolve effective consent state: prefer the window global (set by
        // consent.js when it executes first), fall back to the cookie directly
        // (handles the reload case where this script runs before consent.js).
        var effectiveConsent = window.optiBehaviorConsentState || getConsentCookie();

        if ( privacyMode !== 'full' ) {
            // Anonymous mode: start immediately — no consent required.
            startTracker( 'anonymous' );
        } else if ( effectiveConsent === 'granted' ) {
            // Returning visitor with granted consent — full tracking immediately.
            startTracker( 'granted' );
        } else {
            // Full mode: start anonymous tracking IMMEDIATELY (GDPR Recital 26).
            // Anonymous data (no cookies, no IP) does not require consent.
            startTracker( 'rejected' );

            if ( window.OptiBehaviorDebug ) {
                window.OptiBehaviorDebug.debug( 'Full mode: anonymous tracking started. Listening for consent upgrade...', 'heatmap-simple' );
            }

            // Listen for consent upgrade: if visitor accepts, reload page for full tracking.
            document.addEventListener( 'optibehavior:consent_updated', function( e ) {
                if ( e.detail && e.detail.status === 'granted' ) {
                    if ( window.OptiBehaviorDebug ) {
                        window.OptiBehaviorDebug.debug( 'Consent granted — reloading page for full tracking', 'heatmap-simple' );
                    }
                    window.location.reload();
                }
            } );
        }
    }
    
    /**
     * Heatmap Tracker Class
     */
    function HeatmapTracker(config) {
        this.config = config;
        // GDPR: flag anonymous mode so every method can gate individual-record creation.
        // privacy_mode is forced to 'anonymous' by startTracker() when consent is absent.
        this.isAnonymous = ( config.privacy_mode !== 'full' );
        this.eventQueue = [];
        this.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
        this.lastSendTime = Date.now();
        this.sendTimer = null;
        this.sessionStartTime = Date.now();
        this.maxScrollDepth = 0;
        this.heartbeatTimer = null;
        this.lastScrollUpdate = 0;
        // Store session and visitor IDs in memory for reliable access
        this.sessionId = null;
        this.visitorId = null;
        // Track last activity time for inactivity timeout (30 minutes like Pro version)
        this.lastActivityTime = Date.now();
        this.inactivityTimeout = 30 * 60 * 1000; // 30 minutes in milliseconds
        this.maxSessionDuration = 60 * 60 * 1000; // Cap session at 1 hour max
        // Mouse move tracking state
        this.lastMoveTime = 0;
        this.lastMoveX = 0;
        this.lastMoveY = 0;
        this.moveThrottleMs = 300; // Throttle: record at most every 300ms
        this.moveMinDistance = 30;  // Minimum 30px movement to record
        // Admin bar offset: when admin bar is present, it shifts the page content down.
        // We subtract this offset from Y coordinates so heatmap clicks align with the
        // page as rendered without the admin bar (which is how the heatmap iframe displays).
        var adminBar = document.getElementById('wpadminbar');
        this.adminBarOffset = adminBar ? adminBar.offsetHeight : 0;
    }
    
    /**
     * Get cookie value by name
     */
    HeatmapTracker.prototype.getCookie = function(name) {
        const value = `; ${document.cookie}`;
        const parts = value.split(`; ${name}=`);
        if (parts.length === 2) {
            return parts.pop().split(';').shift();
        }
        return null;
    };

    HeatmapTracker.prototype.start = function() {
        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('start() called', 'heatmap-simple');

        // Send initial page view event immediately to create session
        this.sendPageView();

        // Attach click event listener
        document.addEventListener('click', this.handleClick.bind(this), true);

        // Attach outbound link click tracker
        document.addEventListener('click', this.handleOutboundClick.bind(this), true);

        // Attach mouse move event listener for move/attention heatmap data
        document.addEventListener('mousemove', this.handleMouseMove.bind(this), { passive: true });

        // Attach scroll event listener for scroll depth tracking
        window.addEventListener('scroll', this.handleScroll.bind(this), { passive: true });

        // Calculate initial scroll depth
        this.updateScrollDepth();

        // Record initial viewport position as first breakaway event
        // This captures the above-the-fold area even if user never scrolls
        // Subtract admin bar offset so coordinates match the page without admin bar
        var initialWindowHeight = window.innerHeight - this.adminBarOffset;
        var initialWidth = Math.max(
            document.documentElement.scrollWidth,
            document.body.scrollWidth,
            document.documentElement.clientWidth
        );
        var initialHeight = Math.max(
            document.documentElement.scrollHeight,
            document.body.scrollHeight,
            document.documentElement.clientHeight
        );
        this.eventQueue.push({
            event: 'breakaway',
            device: this.isMobile ? 'mobile' : 'pc',
            x: 0,
            y: Math.floor(initialWindowHeight),
            width: Math.floor(initialWidth),
            height: Math.floor(initialHeight),
            windowHeight: Math.floor(initialWindowHeight),
            time: 0
        });
        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Initial breakaway event recorded at y=' + initialWindowHeight, 'heatmap-simple');

        // Start periodic send timer
        this.startSendTimer();

        // Start heartbeat timer to update session duration and scroll depth
        this.startHeartbeat();

        // Send data before page unload
        window.addEventListener('beforeunload', this.sendSessionEnd.bind(this));

        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Tracking started (clicks, moves, scroll, duration)', 'heatmap-simple');
    };
    
    /**
     * Handle mouse move events for move/attention heatmap data.
     * Throttled to avoid excessive data: records at most every 300ms and only
     * when the mouse has moved at least 30px from the last recorded position.
     */
    HeatmapTracker.prototype.handleMouseMove = function(event) {
        var now = Date.now();

        // Throttle by time
        if (now - this.lastMoveTime < this.moveThrottleMs) {
            return;
        }

        // Get coordinates, adjusting for admin bar offset
        var x = event.pageX || (event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft));
        var y = (event.pageY || (event.clientY + (document.documentElement.scrollTop || document.body.scrollTop))) - this.adminBarOffset;

        // Throttle by distance (minimum movement threshold)
        var dx = x - this.lastMoveX;
        var dy = y - this.lastMoveY;
        if (Math.sqrt(dx * dx + dy * dy) < this.moveMinDistance) {
            return;
        }

        // Update tracking state
        this.lastMoveTime = now;
        this.lastMoveX = x;
        this.lastMoveY = y;
        this.lastActivityTime = now;
        try {
            // Skip localStorage writes in anonymous mode (no storage persistence allowed)
            if ( window.optiBehaviorHeatmapConfig && window.optiBehaviorHeatmapConfig.privacy_mode === 'full' ) {
                localStorage.setItem('optibehavior_last_activity', now.toString());
            }
        } catch (e) {}

        // Get page dimensions
        var width = Math.max(
            document.documentElement.scrollWidth,
            document.body.scrollWidth,
            document.documentElement.clientWidth
        );
        var height = Math.max(
            document.documentElement.scrollHeight,
            document.body.scrollHeight,
            document.documentElement.clientHeight
        );

        // Create event data - sent as 'attention' type which maps to 'moves' folder
        var eventData = {
            event: 'attention',
            device: this.isMobile ? 'mobile' : 'pc',
            x: Math.floor(x),
            y: Math.floor(y),
            width: Math.floor(width),
            height: Math.floor(height),
            time: 0
        };

        // Add to queue
        this.eventQueue.push(eventData);
    };

    HeatmapTracker.prototype.handleClick = function(event) {
        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Click detected!', 'heatmap-simple', event);

        // Update activity timestamp (in memory; localStorage only in full tracking mode)
        this.lastActivityTime = Date.now();
        try {
            // Skip localStorage writes in anonymous mode (no storage persistence allowed)
            if ( window.optiBehaviorHeatmapConfig && window.optiBehaviorHeatmapConfig.privacy_mode === 'full' ) {
                localStorage.setItem('optibehavior_last_activity', this.lastActivityTime.toString());
            }
        } catch (e) {}

        // Get click coordinates, adjusting for admin bar offset so heatmap
        // clicks align with the page as viewed without the admin bar
        const x = event.pageX || (event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft));
        const y = (event.pageY || (event.clientY + (document.documentElement.scrollTop || document.body.scrollTop))) - this.adminBarOffset;

        // Get page dimensions
        const width = Math.max(
            document.documentElement.scrollWidth,
            document.body.scrollWidth,
            document.documentElement.clientWidth
        );
        const height = Math.max(
            document.documentElement.scrollHeight,
            document.body.scrollHeight,
            document.documentElement.clientHeight
        );

        // Create event data
        const eventData = {
            event: 'click', // Send just 'click', PHP will add device suffix
            device: this.isMobile ? 'mobile' : 'pc',
            x: Math.floor(x),
            y: Math.floor(y),
            width: Math.floor(width),
            height: Math.floor(height),
            time: 0 // Will be calculated relative to send time
        };

        // Add to queue
        this.eventQueue.push(eventData);

        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Click recorded:', 'heatmap-simple', eventData);
        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Queue length:', 'heatmap-simple', this.eventQueue.length);

        // Check if we should send immediately
        const timeSinceLastSend = Date.now() - this.lastSendTime;
        const shouldSendNow = this.eventQueue.length >= this.config.ajax_bulk || 
                             timeSinceLastSend >= this.config.ajax_interval;
        
        if (shouldSendNow) {
            this.sendData(false);
        }
    };

    HeatmapTracker.prototype.handleOutboundClick = function(event) {
        // Check if click was on a link or within a link
        let target = event.target;
        let link = null;

        // Traverse up the DOM tree to find a link element
        while (target && target !== document) {
            if (target.tagName === 'A' && target.href) {
                link = target;
                break;
            }
            target = target.parentElement;
        }

        // If no link found, return
        if (!link) {
            return;
        }

        // Get the target URL
        const targetUrl = link.href;
        const sourceUrl = window.location.href;

        // Get click coordinates, adjusting for admin bar offset
        const x = event.pageX || (event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft));
        const y = (event.pageY || (event.clientY + (document.documentElement.scrollTop || document.body.scrollTop))) - this.adminBarOffset;

        // Get element details
        const elementTag = link.tagName.toLowerCase();
        const elementId = link.id || null;
        const elementClass = link.className || null;
        const elementText = link.textContent ? link.textContent.trim().substring(0, 500) : null;

        // Prepare outbound click data
        const outboundData = {
            type: 'outbound_click',
            sourceUrl: sourceUrl,
            targetUrl: targetUrl,
            elementTag: elementTag,
            elementId: elementId,
            elementClass: elementClass,
            elementText: elementText,
            x: Math.floor(x),
            y: Math.floor(y)
        };

        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Outbound click detected:', 'heatmap-simple', outboundData);

        // Send outbound click data immediately
        this.sendOutboundClick(outboundData);
    };

    HeatmapTracker.prototype.sendOutboundClick = function(data) {
        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Sending outbound click:', 'heatmap-simple', data);

        // Use in-memory IDs (set by sendPageView) — works in both anonymous and full modes.
        const sessionId = this.sessionId || '';
        const visitorId = this.visitorId || '';

        // Prepare JSON data for session recording endpoint
        const payload = {
            action: 'outbound_click',
            session_id: sessionId,
            visitor_id: visitorId,
            sourceUrl: data.sourceUrl,
            targetUrl: data.targetUrl,
            elementTag: data.elementTag,
            elementId: data.elementId || null,
            elementClass: data.elementClass || null,
            elementText: data.elementText || null,
            x: data.x,
            y: data.y
        };

        // Prepare form data for session recording endpoint
        const formData = new FormData();
        formData.append('action', 'opti_behavior_heatmap_record');
        formData.append('nonce', this.config.nonce);
        formData.append('data', JSON.stringify(payload));

        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Outbound click payload:', 'heatmap-simple', payload);

        // Use sendBeacon for outbound clicks (more reliable when navigating away)
        // Fallback to fetch if sendBeacon is not supported
        if (navigator.sendBeacon) {
            try {
                const sent = navigator.sendBeacon(this.config.ajax_url, formData);
                if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Outbound click sent via sendBeacon:', 'heatmap-simple', sent);
            } catch(e) {
                if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.error('sendBeacon error, falling back to fetch:', 'heatmap-simple', e);
                // Fallback to fetch
                fetch(this.config.ajax_url, {
                    method: 'POST',
                    body: formData,
                    mode: 'same-origin',
                    cache: 'no-cache',
                    keepalive: true
                }).catch(function(error) {
                    if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.error('Outbound click fetch error:', 'heatmap-simple', error);
                });
            }
        } else {
            // Fallback to fetch with keepalive
            fetch(this.config.ajax_url, {
                method: 'POST',
                body: formData,
                mode: 'same-origin',
                cache: 'no-cache',
                keepalive: true
            }).then(function(response) {
                if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Outbound click sent via fetch, status:', 'heatmap-simple', response.status);
                return response.json();
            }).then(function(responseData) {
                if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Outbound click response:', 'heatmap-simple', responseData);
            }).catch(function(error) {
                if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.error('Outbound click error:', 'heatmap-simple', error);
            });
        }
    };

    HeatmapTracker.prototype.handleScroll = function() {
        // Update activity timestamp (in memory; localStorage only in full tracking mode)
        this.lastActivityTime = Date.now();
        try {
            // Skip localStorage writes in anonymous mode (no storage persistence allowed)
            if ( window.optiBehaviorHeatmapConfig && window.optiBehaviorHeatmapConfig.privacy_mode === 'full' ) {
                localStorage.setItem('optibehavior_last_activity', this.lastActivityTime.toString());
            }
        } catch (e) {}
        this.updateScrollDepth();

        // Queue a breakaway (scroll) event for scroll heatmap data
        // Debounce: only record once every 2 seconds to avoid flooding
        var now = Date.now();
        if (now - this.lastScrollUpdate < 2000) {
            return;
        }
        this.lastScrollUpdate = now;

        var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
        var windowHeight = window.innerHeight;
        var width = Math.max(
            document.documentElement.scrollWidth,
            document.body.scrollWidth,
            document.documentElement.clientWidth
        );
        var height = Math.max(
            document.documentElement.scrollHeight,
            document.body.scrollHeight,
            document.documentElement.clientHeight
        );

        // Record the bottom of the visible viewport as the scroll position
        // This represents how far down the user has seen
        // Subtract admin bar offset so coordinates match the page without admin bar
        var scrollY = Math.floor(scrollTop + windowHeight) - this.adminBarOffset;

        var eventData = {
            event: 'breakaway',
            device: this.isMobile ? 'mobile' : 'pc',
            x: 0,
            y: scrollY,
            width: Math.floor(width),
            height: Math.floor(height),
            windowHeight: Math.floor(windowHeight - this.adminBarOffset),
            time: 0
        };

        this.eventQueue.push(eventData);
        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Scroll breakaway event recorded at y=' + scrollY, 'heatmap-simple');
    };

    HeatmapTracker.prototype.updateScrollDepth = function() {
        const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
        const windowHeight = window.innerHeight;
        const documentHeight = Math.max(
            document.documentElement.scrollHeight,
            document.body.scrollHeight
        );

        const currentScrollDepth = Math.min(100, Math.round(
            ((scrollTop + windowHeight) / documentHeight) * 100
        ));

        if (currentScrollDepth > this.maxScrollDepth) {
            this.maxScrollDepth = currentScrollDepth;
            if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Max scroll depth updated:', 'heatmap-simple', this.maxScrollDepth + '%');
        }
    };

    HeatmapTracker.prototype.startSendTimer = function() {
        // Clear existing timer
        if (this.sendTimer) {
            clearInterval(this.sendTimer);
        }

        // Start new timer
        this.sendTimer = setInterval(function() {
            if (this.eventQueue.length > 0) {
                this.sendData(false);
            }
        }.bind(this), this.config.ajax_interval);
    };

    HeatmapTracker.prototype.startHeartbeat = function() {
        // Clear existing timer
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
        }

        // Send heartbeat every 5 seconds to update session duration and scroll depth
        this.heartbeatTimer = setInterval(function() {
            this.sendHeartbeat();
        }.bind(this), 5000); // 5 seconds
    };

    HeatmapTracker.prototype.sendHeartbeat = function() {
        const now = Date.now();
        const timeSinceLastActivity = now - this.lastActivityTime;
        const totalSessionTime = now - this.sessionStartTime;

        // Stop heartbeat if user has been inactive for 30 minutes
        if (timeSinceLastActivity > this.inactivityTimeout) {
            if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Session inactive for 30+ minutes, stopping heartbeat', 'heatmap-simple');
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = null;
            return;
        }

        // Stop heartbeat if session exceeds 1 hour (prevents inflated durations)
        if (totalSessionTime > this.maxSessionDuration) {
            if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Session exceeded 1 hour max, stopping heartbeat', 'heatmap-simple');
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = null;
            return;
        }

        const sessionDuration = Math.round(totalSessionTime / 1000);

        // Use stored IDs instead of reading cookies
        if (!this.sessionId || !this.visitorId) {
            if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Heartbeat skipped - no session/visitor ID', 'heatmap-simple');
            return;
        }

        const sessionId = this.sessionId;
        const visitorId = this.visitorId;

        const formData = new FormData();
        formData.append('action', 'opti_behavior_heatmap_heartbeat');
        formData.append('nonce', this.config.nonce);
        formData.append('session_id', sessionId);
        formData.append('visitor_id', visitorId);
        formData.append('url', window.location.href);
        formData.append('title', document.title);
        formData.append('duration', sessionDuration);
        formData.append('scroll_depth', this.maxScrollDepth);

        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Sending heartbeat - Duration:', 'heatmap-simple', sessionDuration + 's', 'Scroll:', this.maxScrollDepth + '%');

        fetch(this.config.ajax_url, {
            method: 'POST',
            body: formData,
            mode: 'same-origin',
            cache: 'no-cache',
            keepalive: true
        }).then(function(response) {
            return response.json();
        }).then(function(data) {
            if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Heartbeat response:', 'heatmap-simple', data);
        }).catch(function(error) {
            if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.error('Heartbeat error:', 'heatmap-simple', error);
        });
    };

    HeatmapTracker.prototype.sendSessionEnd = function() {

        const sessionDuration = Math.round((Date.now() - this.sessionStartTime) / 1000);

        // Use stored IDs instead of reading cookies
        if (!this.sessionId || !this.visitorId) {
            return;
        }

        const sessionId = this.sessionId;
        const visitorId = this.visitorId;

        const formData = new FormData();
        formData.append('action', 'opti_behavior_heatmap_heartbeat');
        formData.append('nonce', this.config.nonce);
        formData.append('session_id', sessionId);
        formData.append('visitor_id', visitorId);
        formData.append('url', window.location.href);
        formData.append('title', document.title);
        formData.append('duration', sessionDuration);
        formData.append('scroll_depth', this.maxScrollDepth);

        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Session end - Duration:', 'heatmap-simple', sessionDuration + 's', 'Scroll:', this.maxScrollDepth + '%');

        // Use sendBeacon for reliable delivery on page unload
        if (navigator.sendBeacon) {
            navigator.sendBeacon(this.config.ajax_url, formData);
        }
    };
    
    HeatmapTracker.prototype.sendData = function(isUnload) {
        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('sendData() called, isUnload:', 'heatmap-simple', isUnload);
        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Queue length:', 'heatmap-simple', this.eventQueue.length);

        if (this.eventQueue.length === 0) {
            if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('No events to send', 'heatmap-simple');
            return;
        }

        // Get events to send
        const eventsToSend = this.eventQueue.splice(0);

        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Sending events:', 'heatmap-simple', eventsToSend);

        // Update last send time
        this.lastSendTime = Date.now();

        // Use in-memory IDs (set by sendPageView) — works in both anonymous and full modes.
        const sessionId = this.sessionId || null;
        const visitorId = this.visitorId || null;

        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Session ID:', 'heatmap-simple', sessionId);
        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Visitor ID:', 'heatmap-simple', visitorId);

        // Prepare form data
        const formData = new FormData();
        formData.append('action', this.config.action);
        formData.append('nonce', this.config.nonce);
        formData.append('url', window.location.href);
        formData.append('title', document.title);

        // Add session and visitor IDs (sendData already returns early in anonymous mode).
        if ( sessionId ) {
            formData.append('session_id', sessionId);
        }
        if ( visitorId ) {
            formData.append('visitor_id', visitorId);
        }

        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('AJAX config:', 'heatmap-simple', {
            action: this.config.action,
            nonce: this.config.nonce,
            ajax_url: this.config.ajax_url,
            url: window.location.href,
            title: document.title
        });

        // Add screen resolution data for visitor tracking
        formData.append('screen_width', window.screen.width);
        formData.append('screen_height', window.screen.height);

        // Pass logged-in state from page-render config (reliable)
        // so the server can correctly mark heatmap files as L/G
        if ( this.config.is_logged_in ) {
            formData.append('is_logged_in', '1');
        }

        // Add referrer information from config or document.referrer
        // Use optiBehaviorHeatmapConfig if available, otherwise use config
        const trackingConfig = window.optiBehaviorHeatmapConfig || this.config;

        // Send referrer - filter out internal referrers (same domain = not an external referral)
        let referrer = document.referrer || trackingConfig.referrer || '';
        if (referrer) {
            try {
                var referrerHost = new URL(referrer).hostname;
                var currentHost = window.location.hostname;
                // Strip www. prefix for robust comparison
                if (referrerHost.replace(/^www\./i, '') === currentHost.replace(/^www\./i, '')) {
                    referrer = ''; // Internal referrer, treat as Direct
                }
            } catch (e) {
                // Invalid URL, keep as-is
            }
        }

        if (referrer) {
            formData.append('referrer', referrer);
        }

        // Send UTM parameters if available
        if (trackingConfig.utm_source) {
            formData.append('utm_source', trackingConfig.utm_source);
        }
        if (trackingConfig.utm_medium) {
            formData.append('utm_medium', trackingConfig.utm_medium);
        }
        if (trackingConfig.utm_campaign) {
            formData.append('utm_campaign', trackingConfig.utm_campaign);
        }
        if (trackingConfig.utm_term) {
            formData.append('utm_term', trackingConfig.utm_term);
        }
        if (trackingConfig.utm_content) {
            formData.append('utm_content', trackingConfig.utm_content);
        }

        // Include A/B test variant info if visitor is in an active test.
        // window.optiBehaviorAB is set by the A/B test renderer on the frontend.
        if ( typeof window.optiBehaviorAB !== 'undefined' && window.optiBehaviorAB.length > 0 ) {
            formData.append('ab_test_id', window.optiBehaviorAB[0].test_id);
            formData.append('variant_id', window.optiBehaviorAB[0].variant_id);
        }

        // Add event data
        eventsToSend.forEach(function(event, index) {
            formData.append('data[' + index + '][event]', event.event);
            formData.append('data[' + index + '][device]', event.device); // FIX: Add device field
            formData.append('data[' + index + '][x]', event.x);
            formData.append('data[' + index + '][y]', event.y);
            formData.append('data[' + index + '][width]', event.width);
            formData.append('data[' + index + '][height]', event.height);
            formData.append('data[' + index + '][time]', event.time);
            if (event.windowHeight) {
                formData.append('data[' + index + '][windowHeight]', event.windowHeight);
            }
        });

        // if (this.config.debug) {
        //     window.OptiBehaviorDebug.debug('Sending ', 'heatmap-simple'+ eventsToSend.length + ' events to server');
        //     window.OptiBehaviorDebug.debug('Referrer:', 'heatmap-simple', referrer);
        // }

        // Send data
        if (isUnload && navigator.sendBeacon) {
            // Use sendBeacon for unload events
            if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Using sendBeacon', 'heatmap-simple');
            navigator.sendBeacon(this.config.ajax_url, formData);
        } else {
            // Use fetch for normal sends
            if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Using fetch to send data', 'heatmap-simple');
            fetch(this.config.ajax_url, {
                method: 'POST',
                body: formData,
                mode: 'same-origin',
                cache: 'no-cache',
                keepalive: isUnload
            }).then(function(response) {
                if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Server response status:', 'heatmap-simple', response.status);
                // Handle nonce expiry on cached pages: refresh nonce and re-queue events
                if ( response.status === 403 && !_nonceRefreshed ) {
                    if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug( 'Nonce expired (403), refreshing nonce and re-queuing events', 'heatmap-simple' );
                    refreshNonceAndRequeue( this.config, eventsToSend, this );
                    return null; // Skip JSON parsing
                }
                return response.json();
            }.bind(this)).then(function(data) {
                if ( !data ) { return; } // Nonce refresh was triggered; skip handler
                if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Server response data:', 'heatmap-simple', data);
            }.bind(this)).catch(function(error) {
                if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.error('Send error:', 'heatmap-simple', error);
            });
        }
    };

    /**
     * Send initial page view to create session immediately on page load
     * This ensures sessions are tracked even if user doesn't click anything
     */
    /**
     * Detect browser from user agent
     * Order matters! Check specific browsers before generic ones
     * Chromium-based browsers must be checked before Chrome since they include "Chrome" in UA
     */
    HeatmapTracker.prototype.detectBrowser = function(userAgent) {
        if (!userAgent) return 'Other';

        if (userAgent.indexOf('Edg') > -1) return 'Edge';
        if (userAgent.indexOf('OPR') > -1 || userAgent.indexOf('Opera') > -1) return 'Opera';
        if (userAgent.indexOf('Brave') > -1) return 'Brave';
        if (userAgent.indexOf('Vivaldi') > -1) return 'Vivaldi';
        if (userAgent.indexOf('YaBrowser') > -1) return 'Yandex';
        if (userAgent.indexOf('SamsungBrowser') > -1) return 'Samsung Internet';
        if (userAgent.indexOf('UCBrowser') > -1) return 'UC Browser';
        if (userAgent.indexOf('Chrome') > -1) return 'Chrome';
        if (userAgent.indexOf('Firefox') > -1) return 'Firefox';
        if (userAgent.indexOf('MSIE') > -1 || userAgent.indexOf('Trident') > -1) return 'Internet Explorer';
        if (userAgent.indexOf('Safari') > -1) return 'Safari';

        return 'Other';
    };

    /**
     * Detect operating system from user agent
     * Order matters - check specific patterns before generic ones
     */
    HeatmapTracker.prototype.detectOS = function(userAgent) {
        if (!userAgent) return 'Other';

        // Check iOS first (iPhone/iPad contain "Mac" in some cases)
        if (userAgent.indexOf('iPhone') > -1 || userAgent.indexOf('iPad') > -1 || userAgent.indexOf('iPod') > -1) return 'iOS';
        // Android before Linux (Android contains "Linux")
        if (userAgent.indexOf('Android') > -1) return 'Android';
        // Windows
        if (userAgent.indexOf('Windows NT') > -1 || userAgent.indexOf('Windows Phone') > -1) return 'Windows';
        // macOS (after iOS check)
        if (userAgent.indexOf('Mac OS X') > -1 || userAgent.indexOf('Macintosh') > -1) return 'macOS';
        // Chrome OS
        if (userAgent.indexOf('CrOS') > -1) return 'Chrome OS';
        // HarmonyOS (Huawei)
        if (userAgent.indexOf('HarmonyOS') > -1) return 'HarmonyOS';
        // Linux (generic, after Android)
        if (userAgent.indexOf('Linux') > -1) return 'Linux';
        // Detect bots
        if (/bot|crawl|spider|slurp|googlebot|bingbot|yandex|baidu/i.test(userAgent)) return 'Bot';

        return 'Other';
    };

    /**
     * Detect device type from user agent and screen width
     */
    HeatmapTracker.prototype.detectDeviceType = function(userAgent, screenWidth) {
        if (!userAgent) return 'desktop';

        // Check for mobile indicators in user agent
        const mobileKeywords = ['Mobile', 'Android', 'iPhone', 'iPad', 'iPod', 'BlackBerry', 'IEMobile', 'Opera Mini'];
        const isMobileUA = mobileKeywords.some(function(keyword) {
            return userAgent.indexOf(keyword) > -1;
        });

        // Check for tablet indicators
        const isTablet = userAgent.indexOf('iPad') > -1 ||
                        (userAgent.indexOf('Android') > -1 && userAgent.indexOf('Mobile') === -1);

        if (isTablet) return 'tablet';
        if (isMobileUA) return 'mobile';

        // Fallback to screen width detection
        if (screenWidth <= 768) return 'mobile';
        if (screenWidth <= 1024) return 'tablet';

        return 'desktop';
    };

    HeatmapTracker.prototype.sendPageView = function() {
        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('sendPageView() called - creating session', 'heatmap-simple');

        const now = Date.now();
        const SESSION_TIMEOUT = 1800000; // 30 minutes in milliseconds

        // Resolve privacy mode from the localized config object.
        // Default to 'anonymous' (GDPR-safe) when the key is absent.
        const trackingConfig = window.optiBehaviorHeatmapConfig || this.config;
        const privacyMode = (trackingConfig && trackingConfig.privacy_mode) ? trackingConfig.privacy_mode : 'anonymous';
        const isAnonymous = privacyMode !== 'full';

        let sessionId = null;
        let visitorId = null;

        if (isAnonymous) {
            // Anonymous mode: use SESSION COOKIES to share IDs across all open tabs.
            // Session cookies (no max-age/expires) are auto-deleted when browser closes.
            // No long-term tracking, no consent needed (strictly necessary like PHPSESSID).
            // Cookies are shared across tabs: 1 visitor + 1 session = multiple page views.
            const wpUserId = (trackingConfig && trackingConfig.wp_user_id) ? trackingConfig.wp_user_id : '';
            if (wpUserId) {
                visitorId = wpUserId;
            } else {
                // Check cookie first (shared across tabs), then sessionStorage (backward compat)
                visitorId = this.getCookie('optibehavior_anon_vid');
                if (!visitorId) { try { visitorId = sessionStorage.getItem('optibehavior_anon_vid'); } catch(e) {} }
                if (!visitorId) {
                    visitorId = 'anon_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
                }
                // Store in session cookie (cross-tab) + sessionStorage (backward compat)
                document.cookie = 'optibehavior_anon_vid=' + visitorId + '; path=/; SameSite=Lax';
                try { sessionStorage.setItem('optibehavior_anon_vid', visitorId); } catch(e) {}
            }
            // Check cookie first (shared across tabs), then sessionStorage (backward compat)
            sessionId = this.getCookie('optibehavior_anon_sid');
            if (!sessionId) { try { sessionId = sessionStorage.getItem('optibehavior_anon_sid'); } catch(e) {} }
            if (!sessionId) {
                sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substring(7);
            }
            // Store in session cookie (cross-tab) + sessionStorage (backward compat)
            document.cookie = 'optibehavior_anon_sid=' + sessionId + '; path=/; SameSite=Lax';
            try { sessionStorage.setItem('optibehavior_anon_sid', sessionId); } catch(e) {}
            // Also update last-activity timestamp in localStorage so the Pro recorder
            // (which uses in-memory SafeStorage in anonymous mode) can read it from a
            // NEW tab and avoid treating the shared session as timed-out.
            try { localStorage.setItem('optibehavior_last_activity', Date.now().toString()); } catch(e) {}
            try { localStorage.setItem('optibehavior_session_start', Date.now().toString()); } catch(e) {}
            if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Anonymous mode — session cookies (cross-tab, GDPR-safe)', 'heatmap-simple', { sessionId: sessionId, visitorId: visitorId });
        } else {
            // Full tracking mode: existing cookie/localStorage behavior (unchanged).

            // -----------------------------------------------------------------------
            // Consent-transition migration: anonymous → full
            // When a visitor browsed in anonymous mode and then accepted consent, the
            // page reloads in full mode.  The anonymous tracker stored IDs only in
            // sessionStorage (GDPR-safe, no cookies).  Full mode normally reads from
            // cookies / localStorage and finds nothing → creates brand-new IDs →
            // duplicate visitor + session records in the dashboard.
            //
            // Fix: before touching cookies/localStorage, check sessionStorage for the
            // anonymous IDs.  If found, migrate them into cookies/localStorage so that
            // the existing code below picks them up, and clear the sessionStorage keys
            // to avoid stale data on subsequent page loads.
            // -----------------------------------------------------------------------
            (function migrateAnonIds() {
                var anonSid = null;
                var anonVid = null;
                try { anonSid = sessionStorage.getItem( 'optibehavior_anon_sid' ); } catch(e) {}
                try { anonVid = sessionStorage.getItem( 'optibehavior_anon_vid' ); } catch(e) {}

                if ( anonSid ) {
                    // Migrate anonymous session ID → cookie (same format full mode uses).
                    document.cookie = 'optibehavior_sid=' + anonSid + '; path=/; max-age=1800; SameSite=Lax';
                    try { sessionStorage.removeItem( 'optibehavior_anon_sid' ); } catch(e) {}
                    if ( window.OptiBehaviorDebug ) {
                        window.OptiBehaviorDebug.debug( 'Consent transition: migrated anonymous session ID to cookie', 'heatmap-simple', anonSid );
                    }
                }

                if ( anonVid ) {
                    // Migrate anonymous visitor ID → localStorage (same location full mode uses).
                    try {
                        localStorage.setItem( 'optibehavior_vid', anonVid );
                    } catch(e) {
                        // localStorage unavailable — fall back to cookie.
                        document.cookie = 'optibehavior_vid=' + anonVid + '; path=/; max-age=31536000; SameSite=Lax';
                    }
                    try { sessionStorage.removeItem( 'optibehavior_anon_vid' ); } catch(e) {}
                    if ( window.OptiBehaviorDebug ) {
                        window.OptiBehaviorDebug.debug( 'Consent transition: migrated anonymous visitor ID to localStorage', 'heatmap-simple', anonVid );
                    }
                }
            }());

            // Get session ID from cookie (short-lived, per session)
            sessionId = this.getCookie('optibehavior_sid') || this.getCookie('opti_behavior_session_id');

            // CRITICAL FIX: Check session activity timeout from localStorage
            // Cookie max-age alone doesn't track inactivity - need to validate against last activity
            let lastActivityTime = null;
            try {
                lastActivityTime = parseInt(localStorage.getItem('optibehavior_last_activity') || '0');
            } catch (e) {
                lastActivityTime = 0;
            }

            // Also check Pro's session data for activity (unified activity tracking)
            let proLastActivity = 0;
            try {
                const proSessionData = localStorage.getItem('opti_behavior_pro_session_data');
                if (proSessionData) {
                    const parsed = JSON.parse(proSessionData);
                    proLastActivity = parsed.lastActivity || 0;
                    // If Pro has a session, use the same session ID for consistency
                    if (parsed.sessionId && !sessionId) {
                        sessionId = parsed.sessionId;
                        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Using Pro session ID:', 'heatmap-simple', sessionId);
                    }
                }
            } catch (e) {}

            // Use the most recent activity time from either source
            lastActivityTime = Math.max(lastActivityTime, proLastActivity);

            // If session exists but last activity was more than 30 min ago, expire the session
            if (sessionId && lastActivityTime > 0) {
                const timeSinceLastActivity = now - lastActivityTime;
                if (timeSinceLastActivity >= SESSION_TIMEOUT) {
                    if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Session timed out after 30 min inactivity, creating new session', 'heatmap-simple');
                    sessionId = null; // Force new session creation
                    // Clear old session data
                    try {
                        localStorage.removeItem('optibehavior_session_start');
                        // Also clear Pro session data for consistency
                        localStorage.removeItem('opti_behavior_pro_session_data');
                    } catch (e) {}
                }
            }

            // Logged-in WP user: canonical visitor id is always wp_user_<id>.
            // Mirror the anonymous-mode block so the AB tracker (which reads
            // localStorage first) ships the same stable id regardless of privacy mode.
            // This prevents Cause A (JS full-mode race) from inflating visitor counts.
            const wpUserIdFull = (trackingConfig && trackingConfig.wp_user_id) ? trackingConfig.wp_user_id : '';
            if (wpUserIdFull) {
                visitorId = wpUserIdFull;
                // Write canonical id to localStorage so ab-test-tracker.js reads it too.
                try { localStorage.setItem('optibehavior_vid', visitorId); } catch (e) {}
            } else {
                // Get visitor ID from localStorage (persistent, survives bounce tracker protection)
                // Fallback to cookie for backwards compatibility
                try {
                    visitorId = localStorage.getItem('optibehavior_vid') || this.getCookie('optibehavior_vid') || this.getCookie('opti_behavior_visitor_id');
                } catch (e) {
                    // localStorage not available (privacy mode, old browsers), fallback to cookie
                    visitorId = this.getCookie('optibehavior_vid') || this.getCookie('opti_behavior_visitor_id');
                }
            }

            // Generate IDs if they don't exist
            if (!sessionId) {
                sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substring(7);
                document.cookie = 'optibehavior_sid=' + sessionId + '; path=/; max-age=1800; SameSite=Lax'; // 30 min
                // Store session start time for accurate duration calculation
                try {
                    localStorage.setItem('optibehavior_session_start', now.toString());
                } catch (e) {}
            }

            // Update last activity timestamp (used by both Free and Pro trackers)
            try {
                localStorage.setItem('optibehavior_last_activity', now.toString());
            } catch (e) {}

            if (!visitorId) {
                visitorId = 'visitor_' + Date.now() + '_' + Math.random().toString(36).substring(7);
                // Store visitor ID in localStorage (immune to bounce tracker protection)
                try {
                    localStorage.setItem('optibehavior_vid', visitorId);
                } catch (e) {
                    // localStorage not available, fallback to cookie
                    document.cookie = 'optibehavior_vid=' + visitorId + '; path=/; max-age=31536000; SameSite=Lax'; // 1 year
                }
            }
        }

        // Store IDs in memory for reliable access throughout the session
        this.sessionId = sessionId;
        this.visitorId = visitorId;

        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Page view - Session ID:', 'heatmap-simple', sessionId);
        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Page view - Visitor ID:', 'heatmap-simple', visitorId);

        // Detect device information
        const userAgent = navigator.userAgent;
        const screenWidth = window.screen.width;
        const browser = this.detectBrowser(userAgent);
        const os = this.detectOS(userAgent);
        const deviceType = this.detectDeviceType(userAgent, screenWidth);

        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Device info - Browser:', 'heatmap-simple', browser, 'OS:', os, 'Type:', deviceType);

        // Build the data object matching server's expected format
        const data = {
            action: 'session_start',
            sessionId: sessionId,
            visitorId: visitorId,
            url: window.location.href,
            title: document.title,
            userAgent: userAgent,
            language: navigator.language || navigator.userLanguage || '',
            timezone: (typeof Intl !== 'undefined' && Intl.DateTimeFormat) ? (Intl.DateTimeFormat().resolvedOptions().timeZone || '') : '',
            screen_width: screenWidth,
            screen_height: window.screen.height,
            referrer: (function() {
                var ref = document.referrer || trackingConfig.referrer || '';
                if (ref) {
                    try {
                        var refHost = new URL(ref).hostname.replace(/^www\./i, '');
                        var curHost = window.location.hostname.replace(/^www\./i, '');
                        if (refHost === curHost) { return ''; } // Internal referrer, treat as Direct
                    } catch (e) {}
                }
                return ref;
            })(),
            utm_source: trackingConfig.utm_source || '',
            utm_medium: trackingConfig.utm_medium || '',
            utm_campaign: trackingConfig.utm_campaign || '',
            utm_term: trackingConfig.utm_term || '',
            utm_content: trackingConfig.utm_content || '',
            entry_page: window.location.href,
            device: {
                browser: browser,
                os: os,
                deviceType: deviceType
            }
        };

        // Prepare form data
        const formData = new FormData();
        formData.append('action', 'opti_behavior_heatmap_record');
        formData.append('nonce', this.config.nonce);
        formData.append('data', JSON.stringify(data));

        if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Sending initial session start:', 'heatmap-simple', data);

        // Send page view data immediately
        var self = this;
        fetch(this.config.ajax_url, {
            method: 'POST',
            body: formData,
            mode: 'same-origin',
            cache: 'no-cache'
        }).then(function(response) {
            if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Session start response status:', 'heatmap-simple', response.status);
            // Handle nonce expiry on cached pages: refresh nonce (no events to requeue here)
            if ( response.status === 403 && !_nonceRefreshed ) {
                if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug( 'Nonce expired (403) on session start, refreshing nonce', 'heatmap-simple' );
                refreshNonceAndRequeue( self.config, null, self );
                return null;
            }
            return response.json();
        }).then(function(responseData) {
            if ( !responseData ) { return; }
            if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Session start response data:', 'heatmap-simple', responseData);
            // Fix A: Use server-authoritative visitor_id to eliminate multi-tab race condition.
            // The PHP backend computes a deterministic hash (IP + UA + daily salt) that is
            // identical for every tab of the same browser — no race possible server-side.
            // We overwrite the tentative JS-generated ID with the canonical server value so
            // that all subsequent heartbeats, click events, and outbound-link beacons carry
            // the correct, deduplicated visitor_id.
            if ( responseData.success && responseData.data && responseData.data.visitor_id ) {
                var serverVisitorId = responseData.data.visitor_id;
                if ( serverVisitorId !== self.visitorId ) {
                    if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.debug('Updating visitorId with server-authoritative value:', 'heatmap-simple', serverVisitorId);
                    self.visitorId = serverVisitorId;
                }
                // Persist the authoritative ID back to the matching storage layer so that
                // the next page load (and the Pro recorder) read the correct value without
                // needing another server round-trip.
                if ( isAnonymous ) {
                    // Anonymous mode: session cookie (cross-tab) + sessionStorage (backward compat)
                    document.cookie = 'optibehavior_anon_vid=' + serverVisitorId + '; path=/; SameSite=Lax';
                    try { sessionStorage.setItem('optibehavior_anon_vid', serverVisitorId); } catch(e) {}
                } else {
                    // Full mode: localStorage preferred; cookie fallback (private browsing)
                    try {
                        localStorage.setItem('optibehavior_vid', serverVisitorId);
                    } catch(e) {
                        document.cookie = 'optibehavior_vid=' + serverVisitorId + '; path=/; max-age=31536000; SameSite=Lax';
                    }
                }
            }
        }).catch(function(error) {
            if (window.OptiBehaviorDebug) window.OptiBehaviorDebug.error('Session start error:', 'heatmap-simple', error);
        });
    };

})();

