/**
 * VisitorLens — Session Recorder
 * Tracks user activity and batches events to WP REST API.
 * Requires vlensRecorder localized object from WordPress.
 * Requires vsr-gate.js (window.vlensGateCheck) for consent + URL exclusion.
 */
(function () {
  'use strict';

  if (typeof vlensRecorder === 'undefined') return;

  var gate = window.vlensGateCheck;

  // If URL is excluded, never run
  if (gate && gate.isUrlExcluded && gate.isUrlExcluded()) return;

  var ENDPOINT          = vlensRecorder.endpoint;
  var NONCE             = vlensRecorder.nonce;
  // Admin-configurable save interval (ms). Falls back to 10s and is clamped to
  // the same 6–16s range the settings UI offers. This is the *fastest* the
  // recorder will beacon — see BACKOFF below for how it slows down when
  // there's nothing worth reporting.
  var BASE_FLUSH_INTERVAL = (function () {
    var ms = parseInt(vlensRecorder.flush_ms, 10);
    if (!ms || isNaN(ms)) return 10000;
    return Math.max(6000, Math.min(16000, ms));
  })();
  // Smart throttling: a batch made up only of these "noise" event types
  // (ambient mouse movement / attention heartbeat / bookkeeping) doesn't
  // justify a network round trip on a fixed cadence. Each consecutive
  // noise-only (or empty) flush cycle pushes the next check further out,
  // up to MAX_FLUSH_INTERVAL — a tab that's open but idle drops from one
  // request every ~10s to one every ~60s. Any real interaction (click,
  // input, scroll, error, etc.) snaps the cadence back to BASE_FLUSH_INTERVAL.
  var MAX_FLUSH_INTERVAL  = 60000;
  var BACKOFF_MULTIPLIER  = 1.7;
  var NOISE_TYPES = { mousemove: 1, attention: 1, visibility: 1, pause: 1, resume: 1 };
  var MAX_BATCH         = 150;
  var MOUSEMOVE_SAMPLE  = 200;
  var SCROLL_SAMPLE     = 300;
  var RAGE_CLICK_MS     = 700;
  var RAGE_CLICK_N      = 3;
  var BOT_PATTERN       = /bot|crawl|spider|slurp|headless|phantom/i;
  var INACTIVITY_TIMEOUT  = 20000;
  var INACTIVITY_CHECK_MS = 2000;
  // Admin-configurable "Sampling Rate (%)" — record only a fraction of
  // visitors under heavy traffic. Pageview stats (tracker.js) still cover
  // everyone; only this heavier, continuously-beaconing recorder is sampled.
  var SAMPLING_RATE = (function () {
    var r = parseInt(vlensRecorder.sampling_rate, 10);
    if (isNaN(r)) return 100;
    return Math.max(0, Math.min(100, r));
  })();
  // Small +/-15% spread on every scheduled flush so many concurrent sessions
  // (e.g. a traffic burst) don't all beacon in the same instant.
  var JITTER_FACTOR = 0.15;

  if (BOT_PATTERN.test(navigator.userAgent)) return;
  if (typeof fetch === 'undefined') return;
  if (new URLSearchParams(window.location.search).get('vs_replay') === '1') return;
  if (SAMPLING_RATE < 100 && Math.random() * 100 >= SAMPLING_RATE) return;

  /* ── State (initialized on boot) ── */
  var sessionKey, startTime, sessionId;
  var queue, lastMouse, lastScroll, maxScrollY, clickTimes, pageH;
  var lastActivityAt, isRecording, limitReached, totalPausedMs, pausedAt;
  var flushTimer, inactivityTimer;
  var currentFlushDelay, passiveStreak;
  var booted = false;

  /* ── Keyboard navigation state (accessibility tracking) ── */
  var kbNavTabCount = 0;
  var kbNavEscCount = 0;

  function uuid() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
      var r = Math.random() * 16 | 0;
      return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
    });
  }

  function now() {
    var elapsed = Date.now() - startTime;
    var currentPause = (!isRecording && pausedAt) ? Date.now() - pausedAt : 0;
    return elapsed - totalPausedMs - currentPause;
  }

  function getPageHeight() {
    return Math.max(
      document.body.scrollHeight, document.documentElement.scrollHeight,
      document.body.offsetHeight, document.documentElement.offsetHeight
    );
  }

  function elInfo(el) {
    if (!el || el === document || el === window) return {};
    var tag      = (el.tagName || '').toUpperCase().slice(0, 30);
    var el_id    = (el.id || '').slice(0, 200);
    var classes  = Array.from(el.classList || []).slice(0, 3).join(' ').slice(0, 300);
    var text     = (el.innerText || el.textContent || '').trim().slice(0, 200);
    var href     = el.href ? el.href.slice(0, 2000) : (el.closest && el.closest('a') ? el.closest('a').href : null);
    var itype    = el.type  ? el.type.slice(0, 30)  : null;
    var iname    = (el.name || el.id || el.placeholder || '').slice(0, 200);
    return { tag: tag, el_id: el_id, el_class: classes, el_text: text, href: href, input_type: itype, input_name: iname };
  }

  function push(type, data) {
    if (!isRecording || limitReached) return;
    var e = Object.assign({ type: type, t: now() }, data || {});
    queue.push(e);
    if (queue.length >= MAX_BATCH) flush();
  }

  function markActivity() {
    lastActivityAt = Date.now();
    if (!isRecording) {
      var idleDuration = Date.now() - pausedAt;
      totalPausedMs += idleDuration;
      pausedAt       = null;
      isRecording    = true;
      passiveStreak     = 0;
      currentFlushDelay = BASE_FLUSH_INTERVAL;
      scheduleNextFlush();
      push('resume', { meta: { idle_ms: idleDuration } });
    }
  }

  function inactivityCheck() {
    if (!isRecording) return;
    var idleMs = Date.now() - lastActivityAt;
    if (idleMs >= INACTIVITY_TIMEOUT) {
      push('pause', { meta: { idle_ms: idleMs } });
      flush();
      clearTimeout(flushTimer);
      isRecording = false;
      pausedAt    = Date.now();
    }
  }

  function jittered(ms) {
    var delta = ms * JITTER_FACTOR;
    return Math.round(ms - delta + Math.random() * delta * 2);
  }

  // Self-rescheduling (rather than setInterval) so the cadence can stretch
  // out or snap back between cycles. No-ops while the tab is hidden — the
  // visibilitychange handler below is responsible for resuming it.
  function scheduleNextFlush() {
    clearTimeout(flushTimer);
    if (document.hidden || limitReached) return;
    flushTimer = setTimeout(runFlushCycle, jittered(currentFlushDelay));
  }

  function runFlushCycle() {
    var meaningful = queue.some(function (e) { return !NOISE_TYPES[e.type]; });
    flush().then(function () {
      if (meaningful) {
        passiveStreak     = 0;
        currentFlushDelay = BASE_FLUSH_INTERVAL;
      } else {
        passiveStreak++;
        currentFlushDelay = Math.min(
          MAX_FLUSH_INTERVAL,
          Math.round(BASE_FLUSH_INTERVAL * Math.pow(BACKOFF_MULTIPLIER, passiveStreak))
        );
      }
      scheduleNextFlush();
    });
  }

  async function flush(final) {
    if (queue.length === 0 && sessionId && !final) return;

    var events = queue.splice(0, MAX_BATCH);
    pageH = getPageHeight();

    var scrollPct = pageH > 0 ? Math.min(100, Math.round((maxScrollY + window.innerHeight) / pageH * 100)) : 0;

    var visitorId = null;
    try { visitorId = sessionStorage.getItem('vs_visitor_id'); } catch(e) {}

    var payload = {
      session_key:    sessionKey,
      session_id:     sessionId,
      visitor_id:     visitorId ? parseInt(visitorId, 10) : null,
      page_uri:       window.location.pathname + window.location.search,
      page_title:     document.title || '',
      viewport_w:     window.innerWidth,
      viewport_h:     window.innerHeight,
      page_h:         pageH,
      max_scroll_px:  maxScrollY,
      max_scroll_pct: scrollPct,
      duration_ms:    now(),
      is_final:       final ? 1 : 0,
      events:         events,
    };

    try {
      var res = await fetch(ENDPOINT, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-WP-Nonce':   NONCE,
        },
        body: JSON.stringify(payload),
        keepalive: true,
      });
      var rdata = {};
      try { rdata = await res.json(); } catch (_) {}
      if (res.ok) {
        if (rdata.session_id) sessionId = rdata.session_id;
      } else if (rdata.error === 'limit_reached') {
        limitReached = true;
        clearTimeout(flushTimer);
        queue.length = 0;
      } else if (res.status === 429 || res.status === 403) {
        // Transient rejection (rate-limited / origin edge case) — requeue instead of losing the batch.
        requeueEvents(events);
      }
      // Other error statuses (e.g. malformed payload) are not retried — retrying would loop forever.
    } catch (_) {
      // Network failure — requeue instead of losing the batch.
      requeueEvents(events);
    }
  }

  // Put a failed batch back at the front of the queue, capped so a persistent
  // failure can't grow the in-memory queue without bound.
  function requeueEvents(events) {
    queue = events.concat(queue);
    var cap = MAX_BATCH * 3;
    if (queue.length > cap) queue.splice(0, queue.length - cap);
  }

  /* ── Dead click detection helpers ── */

  function isInteractiveEl(el) {
    if (!el || !el.tagName) return false;
    var tag = el.tagName.toUpperCase();
    if (['A', 'BUTTON', 'INPUT', 'TEXTAREA', 'SELECT', 'LABEL', 'SUMMARY', 'DETAILS', 'VIDEO', 'AUDIO'].indexOf(tag) !== -1) return true;
    if (el.hasAttribute('onclick') || el.hasAttribute('role') && ['button', 'link', 'tab', 'menuitem', 'checkbox', 'radio'].indexOf(el.getAttribute('role')) !== -1) return true;
    if (el.hasAttribute('tabindex') && el.getAttribute('tabindex') !== '-1') return true;
    if (el.closest && (el.closest('a') || el.closest('button') || el.closest('[onclick]') || el.closest('[role="button"]'))) return true;
    return false;
  }

  function detectDeadClick(target, clickData) {
    var navigated = false;
    var mutated = false;
    var startUrl = location.href;

    // Watch for DOM mutations briefly
    var obs;
    try {
      obs = new MutationObserver(function () { mutated = true; });
      obs.observe(document.body, { childList: true, subtree: true, attributes: true });
    } catch (e) { return; }

    // Watch for navigation
    var navCheck = function () { navigated = (location.href !== startUrl); };

    setTimeout(function () {
      obs.disconnect();
      navCheck();
      if (!navigated && !mutated) {
        push('dead_click', {
          x: clickData.x, y: clickData.y,
          scroll_x: clickData.scroll_x, scroll_y: clickData.scroll_y,
          vp_w: clickData.vp_w, vp_h: clickData.vp_h,
          tag: clickData.tag, el_id: clickData.el_id, el_class: clickData.el_class,
          el_text: clickData.el_text, href: clickData.href,
        });
      }
    }, 1000);
  }

  /* ── U-Turn detection helpers ── */

  var lastNavTime = Date.now();
  var lastNavUrl  = location.href;
  var UTURN_THRESHOLD = 5000;

  function checkUturn(newUrl) {
    var timeSinceNav = Date.now() - lastNavTime;
    var prevUrl = lastNavUrl;
    lastNavUrl  = newUrl;
    lastNavTime = Date.now();

    if (timeSinceNav <= UTURN_THRESHOLD && prevUrl !== newUrl) {
      push('u_turn', {
        meta: {
          from_url: prevUrl.slice(0, 500),
          to_url: newUrl.slice(0, 500),
          time_on_page_ms: timeSinceNav,
        },
      });
    }
  }

  /* ── Event listeners (attached once on boot) ── */

  function attachListeners() {
    // SCROLL
    window.addEventListener('scroll', function () {
      markActivity();
      var sy = window.scrollY || window.pageYOffset;
      if (sy > maxScrollY) maxScrollY = sy;
      var t = Date.now();
      if (t - lastScroll < SCROLL_SAMPLE) return;
      lastScroll = t;
      push('scroll', { scroll_x: window.scrollX|0, scroll_y: sy|0, vp_w: window.innerWidth, vp_h: window.innerHeight });
    }, { passive: true });

    // CLICK (with dead click detection)
    document.addEventListener('click', function (e) {
      markActivity();
      var t = Date.now();
      clickTimes.push(t);
      while (clickTimes.length && t - clickTimes[0] > RAGE_CLICK_MS) clickTimes.shift();
      var info   = elInfo(e.target);
      var isRage = clickTimes.length >= RAGE_CLICK_N;
      var clickType = isRage ? 'rage_click' : 'click';
      var clickData = {
        x: Math.round(e.pageX), y: Math.round(e.pageY),
        scroll_x: window.scrollX|0, scroll_y: window.scrollY|0,
        vp_w: window.innerWidth, vp_h: window.innerHeight,
        tag: info.tag, el_id: info.el_id, el_class: info.el_class,
        el_text: info.el_text, href: info.href,
        input_type: info.input_type, input_name: info.input_name,
      };
      push(clickType, clickData);

      // Dispatch custom event for premium survey widget to listen on
      if (isRage) {
        try { window.dispatchEvent(new CustomEvent('vsr:rage_click')); } catch (_) {}
      }

      // Dead click detection: skip if rage click or interactive element
      if (!isRage && !isInteractiveEl(e.target)) {
        detectDeadClick(e.target, clickData);
      }
    }, true);

    // MOUSEMOVE
    document.addEventListener('mousemove', function (e) {
      markActivity();
      var t = Date.now();
      if (t - lastMouse < MOUSEMOVE_SAMPLE) return;
      lastMouse = t;
      push('mousemove', { x: Math.round(e.pageX), y: Math.round(e.pageY), vp_w: window.innerWidth, vp_h: window.innerHeight });
    }, { passive: true });

    // KEYDOWN — standard activity tracking + keyboard navigation events (accessibility)
    document.addEventListener('keydown', function (e) {
      markActivity();
      var key = e.key;
      if (key === 'Tab') {
        kbNavTabCount++;
        var focused = document.activeElement;
        var info    = focused ? elInfo(focused) : {};
        push('keyboard_nav', {
          meta: {
            key:        e.shiftKey ? 'Shift+Tab' : 'Tab',
            tab_total:  kbNavTabCount,
            target_tag: info.tag      || '',
            target_id:  info.el_id   || '',
          },
        });
      } else if (key === 'Escape') {
        kbNavEscCount++;
        push('keyboard_nav', {
          meta: { key: 'Escape', esc_total: kbNavEscCount },
        });
      }
    }, { passive: true });

    // INPUT
    document.addEventListener('input', function (e) {
      markActivity();
      var el = e.target;
      if (!el || !el.tagName) return;
      var tag = el.tagName.toUpperCase();
      if (['INPUT','TEXTAREA','SELECT'].indexOf(tag) === -1) return;
      if (['password','credit-card','cc-number'].indexOf((el.type||'').toLowerCase()) !== -1) return;
      push('input', {
        x: Math.round(el.getBoundingClientRect().left + window.scrollX),
        y: Math.round(el.getBoundingClientRect().top  + window.scrollY),
        tag: tag, input_type: el.type||null, input_name: (el.name||el.id||el.placeholder||'').slice(0,200),
        el_id: (el.id||'').slice(0,200), el_class: Array.from(el.classList||[]).slice(0,3).join(' ').slice(0,300),
      });
    }, { passive: true });

    // FOCUS / BLUR
    document.addEventListener('focusin', function (e) {
      markActivity();
      var info = elInfo(e.target);
      if (!info.tag) return;
      var rect = e.target.getBoundingClientRect ? e.target.getBoundingClientRect() : {};
      push('focus', { x: Math.round((rect.left || 0) + window.scrollX),
                      y: Math.round((rect.top  || 0) + window.scrollY),
                      tag: info.tag, el_id: info.el_id, el_class: info.el_class,
                      el_text: info.el_text, href: info.href,
                      input_type: info.input_type, input_name: info.input_name });
    }, { passive: true });

    document.addEventListener('focusout', function (e) {
      var info = elInfo(e.target);
      if (!info.tag) return;
      push('blur', { tag: info.tag, el_id: info.el_id, el_class: info.el_class,
                     el_text: info.el_text, href: info.href,
                     input_type: info.input_type, input_name: info.input_name });
    }, { passive: true });

    // TEXT SELECTION
    document.addEventListener('selectionchange', function () {
      var sel = window.getSelection();
      if (!sel || sel.isCollapsed) return;
      var text = sel.toString().trim().slice(0, 200);
      if (!text) return;
      markActivity();
      push('select', { el_text: text });
    }, { passive: true });

    // COPY / PASTE
    document.addEventListener('copy',  function () { markActivity(); push('copy');  }, { passive: true });
    document.addEventListener('paste', function () { markActivity(); push('paste'); }, { passive: true });

    // RESIZE
    var resizeTimer;
    window.addEventListener('resize', function () {
      markActivity();
      clearTimeout(resizeTimer);
      resizeTimer = setTimeout(function () {
        push('resize', { vp_w: window.innerWidth, vp_h: window.innerHeight, meta: { page_h: getPageHeight() } });
      }, 300);
    }, { passive: true });

    // VISIBILITY — a backgrounded tab gets zero beacons: stop polling
    // entirely and send whatever's queued in one last shot. Coming back to
    // the foreground resumes at the normal (non-backed-off) cadence and
    // picks up anything that piled up while hidden.
    document.addEventListener('visibilitychange', function () {
      push('visibility', { meta: { hidden: document.hidden } });
      if (document.hidden) {
        clearTimeout(flushTimer);
        flush();
      } else {
        markActivity();
        passiveStreak     = 0;
        currentFlushDelay = BASE_FLUSH_INTERVAL;
        scheduleNextFlush();
      }
    });

    // JS ERRORS (with full stack trace)
    window.addEventListener('error', function (e) {
      var stack = '';
      if (e.error && e.error.stack) {
        stack = e.error.stack.toString().slice(0, 5000);
      }
      push('error', {
        href: window.location.href.slice(0, 2000),
        meta: {
          message: (e.message || '').slice(0, 500),
          source:  (e.filename || '').slice(0, 500),
          line:    e.lineno,
          col:     e.colno,
          stack:   stack,
          url:     window.location.href.slice(0, 2000),
        }
      });
    });

    // UNHANDLED PROMISE REJECTIONS
    window.addEventListener('unhandledrejection', function (e) {
      var message = 'Unhandled Promise Rejection';
      var stack = '';
      if (e.reason) {
        if (typeof e.reason === 'string') {
          message = e.reason.slice(0, 500);
        } else if (e.reason.message) {
          message = e.reason.message.toString().slice(0, 500);
        }
        if (e.reason.stack) {
          stack = e.reason.stack.toString().slice(0, 5000);
        }
      }
      push('error', {
        href: window.location.href.slice(0, 2000),
        meta: {
          message: message,
          source:  'unhandledrejection',
          line:    0,
          col:     0,
          stack:   stack,
          url:     window.location.href.slice(0, 2000),
        }
      });
    });

    // SPA NAVIGATION (with u-turn detection)
    var _push    = history.pushState.bind(history);
    var _replace = history.replaceState.bind(history);
    history.pushState = function () { markActivity(); _push.apply(history, arguments); checkUturn(location.href); push('navigate', { meta: { url: location.href.slice(0, 500) } }); };
    history.replaceState = function () { markActivity(); _replace.apply(history, arguments); checkUturn(location.href); push('navigate', { meta: { url: location.href.slice(0, 500) } }); };
    window.addEventListener('popstate', function () { markActivity(); checkUturn(location.href); push('navigate', { meta: { url: location.href.slice(0, 500) } }); });

    // FORM SUBMIT — captures native form submissions with form ID + method
    document.addEventListener('submit', function (e) {
      markActivity();
      var form = e.target;
      if (!form || form.tagName !== 'FORM') return;
      var formId   = (form.id || form.getAttribute('name') || '').slice(0, 200);
      var action   = (form.action || window.location.href).slice(0, 500);
      var method   = (form.method || 'get').toUpperCase();
      var fields   = form.elements ? form.elements.length : 0;
      push('form_submit', {
        el_id:  formId,
        href:   action,
        meta: {
          method: method,
          action: action,
          fields: fields,
        },
      });
    }, true);

    // ATTENTION TRACKING — IntersectionObserver on 100px bands
    (function () {
      if (typeof IntersectionObserver === 'undefined') return;

      var BAND_HEIGHT = 100;
      var FLUSH_ATT_INTERVAL = 15000; // emit attention events every 15s (a heartbeat, not worth polling faster)
      var bandTimes = {}; // bandIndex → accumulated visible ms
      var visibleBands = {}; // bandIndex → timestamp when it became visible
      var attentionBands = []; // DOM elements we created
      var attContainer = document.createElement('div');
      attContainer.style.cssText = 'position:absolute;top:0;left:0;width:1px;pointer-events:none;z-index:-1;opacity:0;';
      document.body.appendChild(attContainer);

      function ensureBands() {
        var ph = getPageHeight();
        var needed = Math.ceil(ph / BAND_HEIGHT);
        if (needed <= attentionBands.length) return;
        for (var i = attentionBands.length; i < needed; i++) {
          var el = document.createElement('div');
          el.style.cssText = 'position:absolute;top:' + (i * BAND_HEIGHT) + 'px;height:' + BAND_HEIGHT + 'px;width:1px;';
          el.setAttribute('data-band', i);
          attContainer.appendChild(el);
          attentionBands.push(el);
          attObserver.observe(el);
        }
      }

      var attObserver = new IntersectionObserver(function (entries) {
        var ts = Date.now();
        entries.forEach(function (entry) {
          var idx = parseInt(entry.target.getAttribute('data-band'), 10);
          if (entry.isIntersecting) {
            if (!visibleBands[idx]) visibleBands[idx] = ts;
          } else {
            if (visibleBands[idx]) {
              bandTimes[idx] = (bandTimes[idx] || 0) + (ts - visibleBands[idx]);
              delete visibleBands[idx];
            }
          }
        });
      }, { threshold: 0 });

      function flushAttention() {
        var ts = Date.now();
        // Accumulate currently-visible bands
        for (var idx in visibleBands) {
          bandTimes[idx] = (bandTimes[idx] || 0) + (ts - visibleBands[idx]);
          visibleBands[idx] = ts;
        }
        // Only emit bands with >0 time
        var bands = [];
        for (var b in bandTimes) {
          if (bandTimes[b] > 0) {
            bands.push({ band: parseInt(b, 10), ms: bandTimes[b] });
          }
        }
        if (bands.length) {
          push('attention', { meta: { band_height: BAND_HEIGHT, page_h: getPageHeight(), bands: bands } });
        }
        bandTimes = {};
      }

      ensureBands();
      // Re-check for new bands on scroll/resize
      window.addEventListener('scroll', ensureBands, { passive: true });
      window.addEventListener('resize', ensureBands, { passive: true });
      // Skip while hidden — nothing is visibly gaining dwell time in a
      // backgrounded tab, so there's no reason to even compute this.
      setInterval(function () { if (!document.hidden) flushAttention(); }, FLUSH_ATT_INTERVAL);
    })();

    // FINAL FLUSH (with u-turn check on full navigation)
    function finalFlush() {
      clearTimeout(flushTimer);
      // Check for u-turn: if leaving within threshold of page arrival
      var timeOnPage = Date.now() - lastNavTime;
      if (timeOnPage <= UTURN_THRESHOLD) {
        push('u_turn', {
          meta: {
            from_url: location.href.slice(0, 500),
            to_url: '(navigated away)',
            time_on_page_ms: timeOnPage,
          },
        });
      }
      push('visibility', { meta: { hidden: true, final: true } });
      flush(true);
    }
    window.addEventListener('beforeunload', finalFlush);
  }

  /* ── Performance capture ── */

  function capturePerformance() {
    try {
      var nav = performance.getEntriesByType('navigation');
      var paint = performance.getEntriesByType('paint');
      var loadTime = 0, domContentLoaded = 0, firstPaint = 0, firstContentfulPaint = 0;

      if (nav && nav.length) {
        var n = nav[0];
        loadTime         = Math.round(n.loadEventEnd > 0 ? n.loadEventEnd - n.startTime : 0);
        domContentLoaded = Math.round(n.domContentLoadedEventEnd > 0 ? n.domContentLoadedEventEnd - n.startTime : 0);
      } else if (performance.timing) {
        var t = performance.timing;
        loadTime         = t.loadEventEnd > 0 ? t.loadEventEnd - t.navigationStart : 0;
        domContentLoaded = t.domContentLoadedEventEnd > 0 ? t.domContentLoadedEventEnd - t.navigationStart : 0;
      }

      if (paint && paint.length) {
        paint.forEach(function (e) {
          if (e.name === 'first-paint') firstPaint = Math.round(e.startTime);
          if (e.name === 'first-contentful-paint') firstContentfulPaint = Math.round(e.startTime);
        });
      }

      if (loadTime > 0) {
        push('performance', { meta: { loadTime: loadTime, domContentLoaded: domContentLoaded, firstPaint: firstPaint, firstContentfulPaint: firstContentfulPaint } });
      }
    } catch (e) {}
  }

  /* ── Boot: initialize state, attach listeners, start timers ── */

  function boot() {
    if (booted) return;
    booted = true;

    sessionKey     = uuid();
    startTime      = Date.now();
    sessionId      = null;
    queue          = [];
    lastMouse      = 0;
    lastScroll     = 0;
    maxScrollY     = 0;
    clickTimes     = [];
    pageH          = 0;
    lastActivityAt = Date.now();
    isRecording    = true;
    limitReached   = false;
    totalPausedMs  = 0;
    pausedAt       = null;
    kbNavTabCount  = 0;
    kbNavEscCount  = 0;

    currentFlushDelay = BASE_FLUSH_INTERVAL;
    passiveStreak     = 0;
    scheduleNextFlush();
    inactivityTimer = setInterval(inactivityCheck, INACTIVITY_CHECK_MS);

    attachListeners();

    // Performance capture
    if (document.readyState === 'complete') {
      setTimeout(capturePerformance, 0);
    } else {
      window.addEventListener('load', function () { setTimeout(capturePerformance, 0); });
    }

    // Initial flush after ~1s (jittered so a burst of simultaneous page
    // loads doesn't all hit the server in the same instant).
    setTimeout(flush, jittered(1000));
  }

  // Wait for consent before booting
  if (gate && gate.onConsent) {
    gate.onConsent(boot);
  } else {
    boot();
  }

  // Public API
  window.vsRecord = function (type, data) {
    if (!booted) return;
    markActivity();
    push(type, data || {});
  };
})();
