/**
 * AIEO Reco Observer — Phase B impression + click logger.
 *
 * Logs two event types into wp_aieo_reco_events:
 *   - impression  (a recommendation card entered the viewport, once per card per page view)
 *   - reco_click  (user clicked a recommendation card to navigate — NOT the ADD TO BAG button,
 *                  which is already captured by aieo-attribution-client.js)
 *
 * Events are batched (to keep request volume low on pages with many
 * cards) and flushed via `navigator.sendBeacon` every 5 seconds and
 * on `pagehide`. The server absolute-timestamps each event using
 * `request_time - ts_offset_ms` so client clock skew is irrelevant.
 *
 * Impression dedupe is per-(surface,target_pid) and reset on each
 * page load, so a card scrolled into view twice counts as one
 * impression — but the same target appearing on archive AND on a
 * product page in a different visit each counts as a separate
 * impression (different page load = different observer instance).
 */
(function () {
    'use strict';

    var CFG = window.aieoRecoObserver || {};
    if (!CFG.enabled) return;
    if (!('IntersectionObserver' in window)) return; // graceful no-op on ancient browsers

    var AJAX_URL       = CFG.ajaxUrl || (window.ajaxurl || '/wp-admin/admin-ajax.php');
    var FLUSH_EVERY_MS = parseInt(CFG.flushIntervalMs, 10) || 5000;
    var MAX_BATCH      = parseInt(CFG.maxBatchSize, 10) || 40;

    var PAGE_START = Date.now();
    var queue      = [];
    var seenImp    = Object.create(null); // "surface|pid" → true
    var flushTimer = null;

    // PageSpeed-friendly: keep the impression-flush admin-ajax POST out
    // of the page-load critical chain. Two-stage defer:
    //   1) Wait for first real user interaction (click/keydown/touchstart).
    //      We deliberately do NOT treat scroll or mousemove as intent:
    //      synthetic perf tools fire them implicitly — Lighthouse does a
    //      programmatic scroll + cursor mousemove, and GTmetrix scrolls the
    //      whole page for its full-page capture — which would otherwise
    //      release the brake mid-measurement and drag out "fully loaded".
    //      Scroll-only real sessions still flush on pagehide/visibilitychange.
    //   2) Once unlocked, schedule flush via requestIdleCallback with a
    //      generous timeout. The browser only runs idle callbacks when
    //      the main thread has slack, and Lighthouse keeps it busy during
    //      its measurement window. Falls back to setTimeout on browsers
    //      without rIC (Safari < 16).
    // pagehide/visibilitychange flushes still run regardless so we never
    // lose impressions on bounce visits.
    var userInteracted = false;
    function scheduleIdleFlush() {
        if (flushTimer) return;
        if (typeof window.requestIdleCallback === 'function') {
            flushTimer = window.requestIdleCallback(function () {
                flushTimer = null;
                flush();
            }, { timeout: 30000 });
        } else {
            flushTimer = setTimeout(flush, FLUSH_EVERY_MS);
        }
    }
    function releaseFlushBrake() {
        if (userInteracted) return;
        userInteracted = true;
        if (queue.length) { scheduleIdleFlush(); }
    }
    ['click', 'keydown', 'touchstart'].forEach(function (t) {
        window.addEventListener(t, releaseFlushBrake, { capture: true, once: true, passive: true });
    });

    // --- UUID v4 ---------------------------------------------------------

    function uuidV4() {
        if (window.crypto && typeof window.crypto.randomUUID === 'function') {
            try { return window.crypto.randomUUID(); } catch (e) { /* fallthrough */ }
        }
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
            var r = Math.random() * 16 | 0;
            var v = c === 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    }

    // --- Context resolution ---------------------------------------------

    function bodyClasses() {
        return (document.body && document.body.classList) ? document.body.classList : { contains: function () { return false; } };
    }

    /**
     * The host context the card is rendered IN. For single-product
     * pages this is the main product's ID; for the side cart it's the
     * current seed; elsewhere 0.
     */
    /**
     * The host context a specific card is rendered IN. Takes the card
     * element so we can correctly resolve the anchor:
     *   - card inside .aieo-sc-root  → the side-cart seed
     *   - card on a single-product page → the main product's id
     *   - everywhere else (archive / home / cart / checkout) → 0
     *
     * Previously this looked for `[data-aieo-seed-pid]` anywhere in
     * the DOM, which "bled" the side-cart seed onto cards that were
     * NOT inside the side cart (because the side-cart root lives at
     * the bottom of the page and persists across navigations).
     */
    function currentAnchorPid(cardEl) {
        // In-side-cart cards: use the side-cart seed, which lives on
        // the side-cart root element (stamped by aieo-side-cart.js
        // in hydrateRecommendations()).
        if (cardEl) {
            for (var n = cardEl; n && n !== document; n = n.parentNode) {
                if (!n.classList) continue;
                if (n.classList.contains('aieo-sc-root')) {
                    var seedEl = n.querySelector('[data-aieo-seed-pid]') || n;
                    var sp = parseInt(seedEl.getAttribute('data-aieo-seed-pid'), 10);
                    return sp > 0 ? sp : 0;
                }
            }
        }
        var bc = bodyClasses();
        if (bc.contains('single-product')) {
            var m = document.body.className.match(/postid-(\d+)/);
            if (m) return parseInt(m[1], 10) || 0;
        }
        return 0;
    }

    /**
     * Infer the surface of a given recommendation card. Order mirrors
     * aieo-attribution-client.js so sources stay consistent across the
     * impression, click, and ADD rows.
     *
     * ID-based detection runs FIRST — this site renders all five
     * product-page recommendation blocks (needs / upsells / cross-sells
     * / related / recently-viewed) via lazy placeholder sections that
     * all share `class="related products ..."`, so class-based
     * detection would collapse them all into `product_related`. We
     * disambiguate by the block's placeholder id.
     */
    function inferSurface(el) {
        for (var n = el; n && n !== document; n = n.parentNode) {
            if (!n.classList && !n.id && !(n.getAttribute)) continue;
            // Side-cart specific containers (class-based, unambiguous).
            if (n.classList) {
                if (n.classList.contains('aieo-sc-fav-list'))      return 'sidecart_favorites';
                if (n.classList.contains('aieo-sc-rv-list'))       return 'sidecart_recently_viewed';
                if (n.classList.contains('aieo-sc-maylike-slide')) return readMaylikeSurface(n);
                if (n.classList.contains('aieo-sc-reco-col') || n.classList.contains('aieo-sc-reco-list')) {
                    return 'sidecart_might_like';
                }
            }
            // Single-product-page recommendation blocks.
            //
            // IMPORTANT: lazy-recommendations.js REPLACES the
            // `<section id="aieo-...-placeholder">` placeholders with
            // `<section class="aieo-recommendations" data-section="<type>">`
            // at hydration time — see lazy-recommendations.js
            // `mountSection()`. So by the time the shopper interacts
            // with a card, the ID is gone; we must read
            // `data-section` first. Placeholder-IDs are checked too
            // as a fallback for the pre-hydration window (rare —
            // observers usually fire AFTER lazy-reco completes).
            if (n.getAttribute) {
                var ds = n.getAttribute('data-section');
                if (ds) {
                    if (ds === 'same_needs')      return 'product_needs';
                    if (ds === 'upsells')         return 'product_upsells';
                    if (ds === 'crosssells')      return 'product_crosssells';
                    if (ds === 'recently_viewed') return 'product_recently_viewed';
                    if (ds === 'related')         return 'product_related';
                }
            }
            if (n.id) {
                if (n.id === 'aieo-same-needs-placeholder' || n.id === 'aieo-same-needs')       return 'product_needs';
                if (n.id === 'aieo-up-sells-placeholder' || n.id === 'aieo-up-sells')           return 'product_upsells';
                if (n.id === 'aieo-cross-sells-placeholder' || n.id === 'aieo-cross-sells')     return 'product_crosssells';
                if (n.id === 'aieo-recently-viewed-placeholder' || n.id === 'aieo-recently-viewed') return 'product_recently_viewed';
                if (n.id === 'aieo-related-placeholder')                                        return 'product_related';
            }
            // Class-based fallback for themes that use standard WC markup.
            if (n.classList) {
                if (n.classList.contains('same-needs'))     return 'product_needs';
                if (n.classList.contains('upsells'))        return 'product_upsells';
                if (n.classList.contains('cross-sells'))    return 'product_crosssells';
                if (n.classList.contains('related'))        return 'product_related';
                // .aieo-recommendations is the post-hydration wrapper; if
                // we reach here without matching data-section above, the
                // section type is unknown — use generic `product_related`
                // rather than silently mislabeling.
                if (n.classList.contains('aieo-recommendations')) return 'product_related';
            }
        }
        var bc = bodyClasses();
        if (bc.contains('single-product'))                 return 'single_product';
        if (bc.contains('home') || bc.contains('front-page')) return 'home';
        if (
            bc.contains('post-type-archive-product')
            || bc.contains('archive')
            || bc.contains('woocommerce-shop')
            || bc.contains('tax-product_cat')
            || bc.contains('tax-product_brand')
            || bc.contains('tax-product_tag')
        ) return 'archive';
        return 'unknown';
    }

    /**
     * The side-cart "you may also like" track is dynamic — it paints
     * from data.needs / data.crosssells / data.related depending on
     * which array the bundle returned. `aieo-side-cart.js` stamps the
     * chosen surface on each slide's ADD TO BAG button via
     * data-aieo-source, so read that back here to stay in sync.
     */
    function readMaylikeSurface(slideEl) {
        var btn = slideEl.querySelector('[data-aieo-source]');
        if (btn) {
            var s = btn.getAttribute('data-aieo-source');
            if (s) return s;
        }
        return 'sidecart_may_also_like';
    }

    /**
     * Compute the card's 1-based position within its recommendation
     * block. Walks up to find the nearest "block" container (one of
     * the known placeholder IDs or a side-cart list) and counts
     * preceding product-card siblings. Returns 0 when no block
     * context is found (e.g. archive loops where position is not
     * a meaningful concept for attribution).
     */
    function positionOf(cardEl) {
        if (!cardEl) return 0;
        var block = null;
        for (var n = cardEl; n && n !== document; n = n.parentNode) {
            if (!n) continue;
            // Post-hydration AIEO recommendations wrapper — this is
            // what lazy-recommendations.js actually produces on the
            // single-product page (see its mountSection()).
            if (n.classList && (
                n.classList.contains('aieo-recommendations')
                || n.classList.contains('aieo-recommendations-grid')
            )) { block = n; break; }
            // Side-cart lists.
            if (n.classList && (
                n.classList.contains('aieo-sc-rv-list')
                || n.classList.contains('aieo-sc-fav-list')
                || n.classList.contains('aieo-sc-reco-list')
            )) { block = n; break; }
            // Stock WC / theme recommendation block classes.
            if (n.classList && (
                n.classList.contains('upsells')
                || n.classList.contains('cross-sells')
                || n.classList.contains('same-needs')
                // .related is kept last — specific classes win.
                || n.classList.contains('related')
            )) { block = n; break; }
            // Pre-hydration placeholders (rare — observer usually
            // fires after lazy-reco replaces these).
            if (n.id && (
                n.id === 'aieo-same-needs-placeholder'
                || n.id === 'aieo-up-sells-placeholder'
                || n.id === 'aieo-cross-sells-placeholder'
                || n.id === 'aieo-related-placeholder'
                || n.id === 'aieo-recently-viewed-placeholder'
            )) { block = n; break; }
        }
        if (!block) return 0;
        // All plausible product-card children — same union the observer
        // uses so we count the actual visible cards, not arbitrary DOM.
        var cards = block.querySelectorAll([
            '.aieo-card[data-product-id]',
            'ul.products > li.product',
            'li[class*="product-type-"]',
            '.product'
        ].join(', '));
        for (var i = 0; i < cards.length; i++) {
            if (cards[i] === cardEl || cards[i].contains(cardEl)) {
                return i + 1; // 1-based position
            }
        }
        return 0;
    }

    /**
     * Read the product id off a card. Handles:
     *  - .aieo-card[data-product-id="123"]           (AIEO DTO cards)
     *  - li.product-type-*[id="product-123"]         (WC loop items)
     *  - .product[data-product_id="123"]             (Botiga-style)
     */
    function readCardPid(el) {
        var pid = el.getAttribute('data-product-id') || el.getAttribute('data-product_id');
        if (pid) { return parseInt(pid, 10) || 0; }
        if (el.id && /product-(\d+)/.test(el.id)) {
            return parseInt(el.id.match(/product-(\d+)/)[1], 10) || 0;
        }
        // Inner button sometimes carries it.
        var inner = el.querySelector('[data-product_id], [data-product-id]');
        if (inner) {
            var ip = inner.getAttribute('data-product_id') || inner.getAttribute('data-product-id');
            if (ip) return parseInt(ip, 10) || 0;
        }
        return 0;
    }

    // --- Queue + flush --------------------------------------------------

    function enqueue(evt) {
        queue.push(evt);
        // Full batch always flushes immediately — at 40 events the cost
        // of holding longer outweighs the critical-chain saving.
        if (queue.length >= MAX_BATCH) { flush(); return; }
        // Otherwise schedule an idle-flush only after user has interacted
        // (see releaseFlushBrake). pagehide / visibilitychange flushes
        // still cover the bounce case for users who never interact.
        if (userInteracted) { scheduleIdleFlush(); }
    }

    function flush() {
        if (flushTimer) {
            // Same handle var serves both rIC + setTimeout schedulers;
            // cancel via whichever is available (browser ignores wrong-type
            // calls but be explicit).
            if (typeof window.cancelIdleCallback === 'function') {
                try { window.cancelIdleCallback(flushTimer); } catch (e) { /* noop */ }
            }
            try { clearTimeout(flushTimer); } catch (e) { /* noop */ }
            flushTimer = null;
        }
        if (!queue.length) return;
        var batch = queue.splice(0, MAX_BATCH);
        var payload = new FormData();
        payload.append('action', 'aieo_reco_log_batch');
        payload.append('events', JSON.stringify(batch));
        if (navigator.sendBeacon) {
            try { navigator.sendBeacon(AJAX_URL, payload); return; } catch (e) { /* fall through */ }
        }
        try {
            var xhr = new XMLHttpRequest();
            xhr.open('POST', AJAX_URL, true);
            if ('keepalive' in xhr) { xhr.keepalive = true; }
            xhr.send(payload);
        } catch (e) { /* swallow — impressions are non-critical */ }
    }

    // --- Observer -------------------------------------------------------

    // Card selectors — every meaningful recommendation / listing
    // surface on the site.
    //   - `.aieo-card[data-product-id]`  → AIEO DTO cards in the side
    //     cart (RV / favorites / maylike / reco-col)
    //   - `.upsells/.cross-sells/.related/.same-needs .product`
    //     → the four single-product-page recommendation blocks
    //   - `ul.products > li.product`   → stock WC archive / category
    //     loops (and most theme-level loops that extend it)
    //   - `li[class*="product-type-"]` → safety net for themes that
    //     render product loops via shortcodes with non-standard `<ul>`
    var CARD_SELECTORS = [
        '.aieo-card[data-product-id]',
        '.upsells .product',
        '.cross-sells .product',
        '.related .product',
        '.same-needs .product',
        '.aieo-recommendations .product',
        'ul.products > li.product',
        'li[class*="product-type-"]',
    ].join(', ');

    var io = new IntersectionObserver(function (entries) {
        for (var i = 0; i < entries.length; i++) {
            var e = entries[i];
            if (!e.isIntersecting) continue;
            var el = e.target;
            var pid = readCardPid(el);
            if (!pid) { io.unobserve(el); continue; }
            var surface = inferSurface(el);
            var key = surface + '|' + pid;
            if (seenImp[key]) { io.unobserve(el); continue; }
            seenImp[key] = true;
            var evt = {
                event_id:     uuidV4(),
                type:         'impression',
                target_pid:   pid,
                anchor_pid:   currentAnchorPid(el),
                surface:      surface,
                position:     positionOf(el),
                ts_offset_ms: Date.now() - PAGE_START,
            };
            // Archive-surface events carry the term context so we can
            // tell "saw on Matia category" vs "saw on Andreia brand".
            // The server injected `aieoRecoObserver.archive` only when
            // the page is actually an archive, so this is safe to
            // attach unconditionally.
            if (surface === 'archive' && CFG.archive && CFG.archive.term_id) {
                evt.anchor_term_id  = CFG.archive.term_id;
                evt.anchor_term_tax = CFG.archive.tax;
            }
            enqueue(evt);
            io.unobserve(el); // one impression per card per page view
        }
    }, {
        root: null,             // viewport
        // 0.1 threshold + a slight negative rootMargin so a card is
        // considered "seen" once ~10% of it has scrolled into view.
        // Previously 0.5 caused tall cards (common for product tiles
        // on mobile) to never trigger because 50% of the card could
        // never be on-screen at once inside a small viewport.
        threshold: 0.1,
        rootMargin: '0px 0px -40px 0px',
    });

    function observeCards(scope) {
        if (!scope || !scope.querySelectorAll) return;
        var cards = scope.querySelectorAll(CARD_SELECTORS);
        for (var i = 0; i < cards.length; i++) {
            io.observe(cards[i]);
        }
    }

    // Initial scan.
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', function () { observeCards(document); });
    } else {
        observeCards(document);
    }

    // Re-scan when the side-cart panel paints new cards. Those live in
    // .aieo-sc-root — we watch its subtree for added nodes and observe
    // any fresh cards.
    var scRoot = document.querySelector('.aieo-sc-root');
    if (scRoot && 'MutationObserver' in window) {
        var mo = new MutationObserver(function (mutations) {
            for (var i = 0; i < mutations.length; i++) {
                var added = mutations[i].addedNodes;
                for (var j = 0; j < added.length; j++) {
                    var n = added[j];
                    if (n && n.nodeType === 1) { observeCards(n); }
                }
            }
        });
        mo.observe(scRoot, { childList: true, subtree: true });
    }

    // --- Reco-click capture --------------------------------------------

    // Listen on document in capture phase so we fire BEFORE any nav
    // happens. Only trigger when the click lands on the card itself
    // (not the ADD TO BAG button inside it — that's already logged
    // by aieo-attribution-client.js).
    document.addEventListener('click', function (e) {
        var t = e.target;
        if (!t) return;
        // Bail out if the click is inside an ADD TO BAG button or the
        // heart toggle — those are NOT reco-clicks, they're actions.
        for (var a = t; a && a !== document; a = a.parentNode) {
            if (!a.classList) continue;
            if (
                a.classList.contains('aieo-sc-rv-add')
                || a.classList.contains('aieo-sc-fav-toggle')
                || a.classList.contains('aieo-sc-maylike-add')
                || a.classList.contains('add_to_cart_button')
                || a.classList.contains('single_add_to_cart_button')
                || a.classList.contains('aieo-quick-view-trigger')
            ) { return; }
        }
        // Walk up to a card element.
        var card = null;
        for (var n = t; n && n !== document; n = n.parentNode) {
            if (!n.classList) continue;
            if (
                n.classList.contains('aieo-card')
                || (
                    n.tagName && n.tagName.toLowerCase() === 'li'
                    && Array.prototype.some.call(n.classList, function (c) { return c.indexOf('product-type-') === 0; })
                )
                || (n.classList.contains('product') && n.querySelector && n.querySelector('a[href*="/product/"]'))
            ) { card = n; break; }
        }
        if (!card) return;
        var pid = readCardPid(card);
        if (!pid) return;
        var surface = inferSurface(card);
        var anchor  = currentAnchorPid(card);
        // Skip self-clicks on a product's own page (e.g., tapping the
        // product's own image/title) — those aren't recommendations.
        if (surface === 'single_product' && anchor === pid) { return; }
        var clickEvt = {
            event_id:     uuidV4(),
            type:         'reco_click',
            target_pid:   pid,
            anchor_pid:   anchor,
            surface:      surface,
            position:     positionOf(card),
            ts_offset_ms: Date.now() - PAGE_START,
        };
        if (surface === 'archive' && CFG.archive && CFG.archive.term_id) {
            clickEvt.anchor_term_id  = CFG.archive.term_id;
            clickEvt.anchor_term_tax = CFG.archive.tax;
        }
        enqueue(clickEvt);
        // Don't prevent default — the link navigation must proceed.
    }, true);

    // --- Lifecycle flushes ---------------------------------------------

    window.addEventListener('pagehide', flush);
    window.addEventListener('beforeunload', flush);
    // visibilitychange catches tab-switch cases where pagehide may not fire.
    document.addEventListener('visibilitychange', function () {
        if (document.visibilityState === 'hidden') { flush(); }
    });
})();
