(
  args = {
    viewportExpansion: 0
  }
) => {
  const { viewportExpansion } = args;

  /***
   * Store expensive element and window function call results.
   */
  const DOM_CACHE = {
    boundingRects: new WeakMap(),
    clientRects: new WeakMap(),
    computedStyles: new WeakMap(),

    ClearCache() {
      this.boundingRects = new WeakMap();
      this.clientRects = new WeakMap();
      this.computedStyles = new WeakMap();
    },

    GetCached(cache, element, getter) {
      if (!element) return null;
      if (cache.has(element)) return cache.get(element);
      const value = getter(element);
      if (value) cache.set(element, value);
      return value;
    },

    GetBoundingRect(element) {
      return this.GetCached(this.boundingRects, element, el => el.getBoundingClientRect());
    },

    GetClientRects(element) {
      return this.GetCached(this.clientRects, element, el => el.getClientRects());
    },

    GetComputedStyle(element) {
      return this.GetCached(this.computedStyles, element, el => (el.ownerDocument?.defaultView || window).getComputedStyle(el));
    }
  };

  /**
   * Hash map of DOM nodes indexed by their highlight index.
   *
   * @type {Object<string, any>}
   */
  const DOM_HASH_MAP = {};
  const ID = { current: 0 };
  const HIGHLIGHT_CONTAINER_ID = "playwright-highlight-container";

  function XPathNodeStep(el) {
    const tag = el.nodeName.toLowerCase();
    const parent = el.parentElement;
    if (!parent) return tag; // root element (html)
    const sameTagSiblings = Array.from(parent.children)
      .filter(c => c.nodeName.toLowerCase() === tag);
    if (sameTagSiblings.length === 1) return tag;
    const index = sameTagSiblings.indexOf(el) + 1; // 1-based index
    return `${tag}[${index}]`;
  }

  function GetLocalXPath(el) {
    const steps = [];
    let cur = el;
    let shadowHostXPath = null;

    while (cur && cur.nodeType === Node.ELEMENT_NODE) {
      const root = cur.getRootNode();
      if (root instanceof ShadowRoot) {
        const host = root.host;
        shadowHostXPath = GetLocalXPath(host).xpathLocal;
        steps.unshift(XPathNodeStep(cur));
        break;
      }
      steps.unshift(XPathNodeStep(cur));
      if (cur === cur.ownerDocument.documentElement) break;
      cur = cur.parentElement;
    }

    return {
      xpathLocal: "/" + steps.join("/"),
      shadowHostXPath
    };
  }

  function IsSameOrigin(win) {
    if (!win) { return false; }
    try {
      void win.document.location.href;
      return true;
    } catch {
      return false;
    }
  }

  function GetFrameChain(el) {
    const chain = [];
    let doc = el?.ownerDocument;
    let crossOrigin = false;

    while (doc && doc.defaultView && doc.defaultView.frameElement) {
      const frameEl = doc.defaultView.frameElement;
      const parentWin = frameEl.ownerDocument.defaultView;

      if (!IsSameOrigin(parentWin)) {
        crossOrigin = true;
        break;
      }

      const { xpathLocal } = GetLocalXPath(frameEl);
      chain.unshift(xpathLocal);

      doc = parentWin.document;
    }

    return { chain, crossOrigin };
  }

  function GenerateXPathData(el) {
    const { xpathLocal, shadowHostXPath } = GetLocalXPath(el);
    const { chain: frameChain, crossOrigin } = GetFrameChain(el);
    let shadowInternalPath = null;
    if (shadowHostXPath) {
      const root = el.getRootNode();
      if (root instanceof ShadowRoot) {
        const steps = [];
        let cur = el;
        while (cur && cur.nodeType === Node.ELEMENT_NODE && !(cur.getRootNode() instanceof Document)) {
          steps.unshift(XPathNodeStep(cur));
          cur = cur.parentElement;
        }
        shadowInternalPath = steps.join(" > ");
      }
    }
    return {
      xpath_local: xpathLocal,
      frame_chain: frameChain,
      cross_origin_frame: crossOrigin,
      shadow_host_xpath: shadowHostXPath,
      shadow_internal_path: shadowInternalPath,
      xpath: xpathLocal // alias for compatibility
    };
  }

  /**
   * Checks if a text node is visible.
   *
   * @param {Text} textNode - The text node to check.
   * @returns {boolean} Whether the text node is visible.
   */
  function IsTextNodeVisible(textNode) {
    try {
      if (!textNode?.parentElement) { return false; }
      // Special case: when viewportExpansion is -1, consider all text nodes as visible
      if (viewportExpansion === -1) {
        // Still check parent visibility for basic filtering
        const parentElement = textNode.parentElement;
        if (!parentElement) return false;

        try {
          return parentElement.checkVisibility({
            checkOpacity: true,
            checkVisibilityCSS: true,
          });
        } catch (e) {
          // Fallback if checkVisibility is not supported
          const style = DOM_CACHE.GetComputedStyle(parentElement);
          return style.display !== 'none' &&
            style.visibility !== 'hidden' &&
            parseFloat(style.opacity || "1") > 0;
        }
      }

      const range = (textNode.ownerDocument || document).createRange();
      range.selectNodeContents(textNode);
      const rects = range.getClientRects(); // Use getClientRects for Range

      if (!rects || rects.length === 0) {
        return false;
      }

      let isAnyRectVisible = false;
      let isAnyRectInViewport = false;

      for (const rect of rects) {
        // Check size
        if (rect.width > 0 && rect.height > 0) {
          isAnyRectVisible = true;

          // Viewport check for this rect
          const vw = textNode.ownerDocument?.defaultView || window;
          if (!(
            rect.bottom < -viewportExpansion ||
            rect.top > vw.innerHeight + viewportExpansion ||
            rect.right < -viewportExpansion ||
            rect.left > vw.innerWidth + viewportExpansion
          )) {
            isAnyRectInViewport = true;
            break; // Found a visible rect in viewport, no need to check others
          }
        }
      }

      if (!isAnyRectVisible || !isAnyRectInViewport) {
        return false;
      }

      // Check parent visibility
      const parentElement = textNode.parentElement;
      if (!parentElement) return false;

      try {
        return parentElement.checkVisibility({
          checkOpacity: true,
          checkVisibilityCSS: true,
        });
      } catch (e) {
        // Fallback if checkVisibility is not supported
        const style = DOM_CACHE.GetComputedStyle(parentElement);
        return style.display !== 'none' &&
          style.visibility !== 'hidden' &&
          parseFloat(style.opacity || "1") > 0;
      }
    } catch (e) {
      console.warn('Error checking text node visibility:', e);
    }
    return false;
  }

  /**
   * Checks if an element is accepted.
   *
   * @param {HTMLElement} element - The element to check.
   * @returns {boolean} Whether the element is accepted.
   */
  function IsElementAccepted(element) {
    if (!element || !element.tagName) return false;

    // Always accept body and common container elements
    const alwaysAccept = new Set([
      "body", "div", "main", "article", "section", "nav", "header", "footer"
    ]);
    const tagName = element.tagName.trim().toLowerCase();

    if (alwaysAccept.has(tagName)) return true;

    const leafElementDenyList = new Set([
      //"svg",
      "script",
      "style",
      "link",
      "meta",
      "noscript",
      "template",
    ]);

    return !leafElementDenyList.has(tagName);
  }

  /**
   * Robust, cache-friendly check for whether an element is truly visible in the DOM.
   * Uses bounding rect, computed style, and ancestor checks.
   *
   * @param {HTMLElement} element - The element to check.
   * @returns {boolean}
   */
  function IsElementReallyVisible(element) {
    if (!element) return false;

    // Check bounding rect (cached)
    const rect = DOM_CACHE.GetBoundingRect(element);
    if (!rect || rect.width === 0 || rect.height === 0) return false;

    // Walk up the ancestor chain to ensure all ancestors are visible
    let current = element;
    while (current && current.nodeType === Node.ELEMENT_NODE) {
      const style = DOM_CACHE.GetComputedStyle(current);
      if (!style) return false;
      if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity || "1") === 0 || style.pointerEvents === 'none') {
        return false;
      }
      current = current.parentElement;
    }

    return true;
  }

  function IsSubtreeVisible(element) {
    if (!element) return false;

    // Walk up the ancestor chain
    let current = element;
    while (current && current.nodeType === Node.ELEMENT_NODE) {
      const style = DOM_CACHE.GetComputedStyle(current);
      if (!style) return false;

      // If any ancestor fully hides its subtree, stop
      if (style.display === 'none' ||
          style.visibility === 'hidden' ||
          parseFloat(style.opacity || "1") === 0 ||
          style.pointerEvents === 'none') {
        return false;
      }

      current = current.parentElement;
    }

    // If no ancestor hides subtree, it's considered visible
    return true;
  }

  /**
   * Checks if an element is the topmost element at its position.
   *
   * @param {HTMLElement} element - The element to check.
   * @returns {boolean} Whether the element is the topmost element at its position.
   */
  function IsTopElement(element) {
    if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
    if (viewportExpansion === -1) return true;

    const rects = DOM_CACHE.GetClientRects(element);
    if (!rects || !rects.length) return false;

    // pick the first rect that's in (expanded) view for THIS element's viewport
    const vw = element.ownerDocument?.defaultView;
    const viewW = vw ? vw.innerWidth : window.innerWidth;
    const viewH = vw ? vw.innerHeight : window.innerHeight;

    let targetRect = null;
    for (const r of rects) {
      if (r.width <= 0 || r.height <= 0) continue;
      const inView = !(
        r.bottom < -viewportExpansion ||
        r.top    > viewH + viewportExpansion ||
        r.right  < -viewportExpansion ||
        r.left   > viewW + viewportExpansion
      );
      if (inView) { targetRect = r; break; }
    }
    if (!targetRect) return false;

    const cx = targetRect.left + targetRect.width  / 2;
    const cy = targetRect.top  + targetRect.height / 2;

    // Prefer the shadow root when present; fall back to ownerDocument
    const root = element.getRootNode();
    const hitCtx = (root instanceof ShadowRoot) ? root : element.ownerDocument;

    try {
      const topEl = hitCtx.elementFromPoint(cx, cy);
      if (!topEl) return false;

      // Walk up to see if 'element' is in the ancestry of the hit target
      let cur = topEl;
      const stop = (root instanceof ShadowRoot) ? root : hitCtx.documentElement;
      while (cur && cur !== stop) {
        if (cur === element) return true;
        cur = cur.parentElement;
      }
      return topEl === element ||
             (topEl.getRootNode &&
              element.getRootNode &&
              topEl.getRootNode() === element.getRootNode()
              && (topEl === element || topEl.closest && !!topEl.closest(tagName)));
    } catch {
      // If something odd happens (e.g., closed shadow root), be permissive
      return true;
    }
  }

  /**
   * Checks if an element is within the expanded viewport.
   *
   * @param {HTMLElement} element - The element to check.
   * @param {number} viewportExpansion - The viewport expansion.
   * @returns {boolean} Whether the element is within the expanded viewport.
   */
  function IsInExpandedViewport(element, viewportExpansion) {
    if (viewportExpansion === -1) {
      return true;
    }

    const rects = DOM_CACHE.GetClientRects(element);
    const vw = element.ownerDocument?.defaultView || window;

    if (!rects || rects.length === 0) {
      // Fallback to getBoundingClientRect if getClientRects is empty,
      // useful for elements like <svg> that might not have client rects but have a bounding box.
      const boundingRect = DOM_CACHE.GetBoundingRect(element);
      if (!boundingRect || boundingRect.width === 0 || boundingRect.height === 0) {
        return false;
      }
      return !(
        boundingRect.bottom < -viewportExpansion ||
        boundingRect.top > vw.innerHeight + viewportExpansion ||
        boundingRect.right < -viewportExpansion ||
        boundingRect.left > vw.innerWidth + viewportExpansion
      );
    }

    // Check if *any* client rect is within the viewport
    for (const rect of rects) {
      if (rect.width === 0 || rect.height === 0) continue; // Skip empty rects

      if (!(
        rect.bottom < -viewportExpansion ||
        rect.top > vw.innerHeight + viewportExpansion ||
        rect.right < -viewportExpansion ||
        rect.left > vw.innerWidth + viewportExpansion
      )) {
        return true; // Found at least one rect in the viewport
      }
    }

    return false; // No rects were found in the viewport
  }

  /**
   * Checks if an element is interactive.
   * 
   * lots of comments, and uncommented code - to show the logic of what we already tried
   * 
   * One of the things we tried at the beginning was also to use event listeners, and other fancy class, style stuff -> what actually worked best was just combining most things with computed cursor style :)
   * 
   * @param {HTMLElement} element - The element to check.
   */
  function IsInteractiveElement(element) {
    if (!element || element.nodeType !== Node.ELEMENT_NODE) { return false; }
    if (element.tagName && element.tagName.trim().toLowerCase() === 'html') { return false; }

    const tagName = element.tagName.trim().toLowerCase();
    const style = DOM_CACHE.GetComputedStyle(element);

    // Early bail-out if pointer-events disables all interaction
    if (style?.pointerEvents === "none") return false;

    const interactiveElements = new Set([
      "a",          // Links
      "button",     // Buttons
      "input",      // All input types (text, checkbox, radio, etc.)
      "select",     // Dropdown menus
      "textarea",   // Text areas
      "details",    // Expandable details
      "summary",    // Summary element (clickable part of details)
      "label",      // Form labels (often clickable)
      "option",     // Select options
      "optgroup",   // Option groups
      "fieldset",   // Form fieldsets (can be interactive with legend)
      "legend",     // Fieldset legends
    ]);

    // Define explicit disable attributes and properties
    const explicitDisableTags = new Set([
      'disabled',           // Standard disabled attribute
      // 'aria-disabled',      // ARIA disabled state
      'readonly',          // Read-only state
      // 'aria-readonly',     // ARIA read-only state
      // 'aria-hidden',       // Hidden from accessibility
      // 'hidden',            // Hidden attribute
      // 'inert',             // Inert attribute
      // 'aria-inert',        // ARIA inert state
      // 'tabindex="-1"',     // Removed from tab order
      // 'aria-hidden="true"' // Hidden from screen readers
    ]);

    const interactiveRoles = new Set([
      'button',          // Directly clickable element
      'link',            // Clickable link
      'menuitem',        // Clickable menu item
      'menuitemradio',   // Radio-style menu item (selectable)
      'menuitemcheckbox', // Checkbox-style menu item (toggleable)
      'radio',           // Radio button (selectable)
      'checkbox',        // Checkbox (toggleable)
      'tab',             // Tab (clickable to switch content)
      'switch',          // Toggle switch (clickable to change state)
      'slider',          // Slider control (draggable)
      'spinbutton',      // Number input with up/down controls
      'combobox',        // Dropdown with text input
      'searchbox',       // Search input field
      'textbox',         // Text input field
      // 'listbox',         // Selectable list
      'option',          // Selectable option in a list
      'scrollbar'        // Scrollable control
    ]);

    // SVG-specific interactive checks
    if (element.namespaceURI === "http://www.w3.org/2000/svg") {
      const tag = element.tagName.toLowerCase();

      // Paths and primitive shapes should never be interactive unless explicitly wired
      if (["path","circle","rect","line","polyline","polygon","g","use"].includes(tag)) {
        // Only allow if dev explicitly added a handler or tabindex
        if (element.hasAttribute("onclick") ||
            element.hasAttribute("tabindex") ||
            typeof element.onclick === "function") {
          return true;
        }
        return false; // stop here
      }

      if (tag === "svg") {
        const svgRole = element.getAttribute("role");
        if (svgRole && interactiveRoles.has(svgRole)) return true;
        if (element.hasAttribute("tabindex")) return true;
      }
    }

    // handle inputs, select, checkbox, radio, textarea, button and make sure they are not cursor style disabled/not-allowed
    if (interactiveElements.has(tagName)) {
      // Check for explicit disable attributes
      for (const disableTag of explicitDisableTags) {
        if (element.hasAttribute(disableTag) ||
          element.getAttribute(disableTag) === 'true' ||
          element.getAttribute(disableTag) === '') {
          return false;
        }
      }

      // Check for disabled property on form elements
      if (element.disabled) {
        return false;
      }

      // Check for readonly property on form elements
      if (element.readOnly) {
        return false;
      }

      // Check for inert property
      if (element.inert) {
        return false;
      }

      return true;
    }

    const role = element.getAttribute("role");

    // Check for contenteditable attribute
    if (element.getAttribute("contenteditable") === "true" || element.isContentEditable) {
      return true;
    }

    // Added enhancement to capture dropdown interactive elements
    if (element.classList && (
      element.classList.contains("button") ||
      element.classList.contains('dropdown-toggle') ||
      element.getAttribute('data-index') ||
      element.getAttribute('data-toggle') === 'dropdown' ||
      element.getAttribute('aria-haspopup') === 'true'
    )) {
      return true;
    }

    // Basic role/attribute checks
    const hasInteractiveRole =
      interactiveElements.has(tagName) ||
      (role && interactiveRoles.has(role));

    if (hasInteractiveRole) return true;


    // check whether element has event listeners by window.getEventListeners
    try {
      if (typeof getEventListeners === 'function') {
        const listeners = getEventListeners(element);
        const mouseEvents = ['click', 'mousedown', 'mouseup', 'dblclick'];
        for (const eventType of mouseEvents) {
          if (listeners[eventType] && listeners[eventType].length > 0) {
            return true; // Found a mouse interaction listener
          }
        }
      }

      const getEventListenersForNode = element?.ownerDocument?.defaultView?.getEventListenersForNode || window.getEventListenersForNode;
      if (typeof getEventListenersForNode === 'function') {
        const listeners = getEventListenersForNode(element);
        const interactionEvents = ['click', 'mousedown', 'mouseup', 'keydown', 'keyup', 'submit', 'change', 'input', 'focus', 'blur'];
        for (const eventType of interactionEvents) {
          for (const listener of listeners) {
            if (listener.type === eventType) {
              return true; // Found a common interaction listener
            }
          }
        }
      }
      // Fallback: Check common event attributes if getEventListeners is not available (getEventListeners doesn't work in page.evaluate context)
      const commonMouseAttrs = ['onclick', 'onmousedown', 'onmouseup', 'ondblclick'];
      for (const attr of commonMouseAttrs) {
        if (element.hasAttribute(attr) || typeof element[attr] === 'function') {
          return true;
        }
      }
    } catch (e) {
      // console.warn(`Could not check event listeners for ${element.tagName}:`, e);
      // If checking listeners fails, rely on other checks
    }

    return false
  }

  /**
   * Heuristically determines if an element should be considered as independently interactive,
   * even if it's nested inside another interactive container.
   *
   * This function helps detect deeply nested actionable elements (e.g., menu items within a button)
   * that may not be picked up by strict interactivity checks.
   *
   * @param {HTMLElement} element - The element to check.
   * @returns {boolean} Whether the element is heuristically interactive.
   */
  function IsHeuristicallyInteractive(element) {
    if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
    if (element.tagName && element.tagName.trim().toLowerCase() === 'html') { return false; }

    // Fast skip for invisible elements
    if (!IsElementReallyVisible(element)) return false;

    // (Optional) Skip top-level wrappers to avoid massive wrappers being flagged
    const isParentBody = element.parentElement && element.parentElement.isSameNode(document.body);
    if (isParentBody) return false;

    // Catch interactive by class/attribute
    const hasInteractiveClass = /\b(btn|clickable|action|menu|entry|link|dropdown)\b/i
                                .test(element.getAttribute ? (element.getAttribute("class") || "") : "");
    const hasInteractiveAttributes =
      element.hasAttribute('role') ||
      element.hasAttribute('tabindex') ||
      element.hasAttribute('onclick') ||
      typeof element.onclick === 'function' ||
      element.hasAttribute('data-action') ||
      element.getAttribute("contenteditable") === "true";

    // SVG edge-case (custom icons/buttons)
    if (
      element.namespaceURI === "http://www.w3.org/2000/svg" &&
      (element.getAttribute("role") === "button" ||
        element.hasAttribute("tabindex") ||
        DOM_CACHE.GetComputedStyle(element).cursor === "pointer")
    ) return true;

    // If it's inside a common interactive container (e.g., dropdown, menu, toolbar)
    const isInKnownContainer = Boolean(
      element.closest('button,a,[role="button"],.menu,.dropdown,.list,.toolbar')
    );

    // Has at least one visible child
    const hasVisibleChildren = [...element.children].some(IsElementReallyVisible);

    // Core heuristic: any of the "looks interactive" signals, and not just a wrapper
    return (
      (hasInteractiveAttributes || hasInteractiveClass) &&
      hasVisibleChildren &&
      isInKnownContainer
    );
  }

  const DISTINCT_INTERACTIVE_TAGS = new Set([
    'a', 'button', 'input', 'select', 'textarea', 'summary', 'details', 'label', 'option'
  ]);
  const INTERACTIVE_ROLES = new Set([
    'button', 'link', 'menuitem', 'menuitemradio', 'menuitemcheckbox',
    'radio', 'checkbox', 'tab', 'switch', 'slider', 'spinbutton',
    'combobox', 'searchbox', 'textbox', 'listbox', 'option', 'scrollbar'
  ]);

  /**
   * Checks if an element likely represents a distinct interaction
   * separate from its parent (if the parent is also interactive).
   *
   * @param {HTMLElement} element - The element to check.
   * @returns {boolean} Whether the element is a distinct interaction.
   */
  function IsElementDistinctInteraction(element, isInteractive) {
    if (!element || element.nodeType !== Node.ELEMENT_NODE) { return false; }
    if (element.tagName && element.tagName.trim().toLowerCase() === 'html') { return false; }

    const tagName = element.tagName.trim().toLowerCase();
    const role = element.getAttribute('role');

    // --- SVG edge case ---
    if (element.namespaceURI === "http://www.w3.org/2000/svg") {
      // Check for role="button", tabindex, or pointer cursor
      const svgRole = element.getAttribute("role");
      if (
        (svgRole && INTERACTIVE_ROLES.has(svgRole)) ||
        (element.hasAttribute("tabindex") && parseInt(element.getAttribute("tabindex")) >= 0) ||
        DOM_CACHE.GetComputedStyle(element).cursor === "pointer"
      ) {
        return true;
      }
      // Continue: other SVGs may still be covered by heuristics below
    }

    // --- Tabbable elements (keyboard-accessible) ---
    if (
      element.hasAttribute('tabindex') &&
      !isNaN(parseInt(element.getAttribute('tabindex'))) &&
      parseInt(element.getAttribute('tabindex')) >= 0
    ) {
      return true;
    }

    // --- iframe is always a distinct interaction boundary ---
    if (tagName === 'iframe') {
      return true;
    }

    // --- Tag/role whitelists (micro-test boundaries) ---
    if (DISTINCT_INTERACTIVE_TAGS.has(tagName)) {
      return true;
    }
    if (role && INTERACTIVE_ROLES.has(role)) {
      return true;
    }

    // --- Contenteditable regions ---
    if (element.isContentEditable || element.getAttribute('contenteditable') === 'true') {
      return true;
    }

    // --- Common automation/testing attributes ---
    if (
      element.hasAttribute('data-testid') ||
      element.hasAttribute('data-cy') ||
      element.hasAttribute('data-test')
    ) {
      return true;
    }

    // --- Explicit event handler (attribute or property) ---
    if (
      element.hasAttribute('onclick') ||
      typeof element.onclick === 'function'
    ) {
      return true;
    }

    // --- Explicit interaction event listeners (if available) ---
    if (isInteractive) {
      return true; // Found a common interaction listener
    }

    // --- Heuristic fallback ---
    if (IsHeuristicallyInteractive(element)) {
      return true;
    }

    // --- Default: not a distinct interaction from parent ---
    return false;
  }

  const JS_FUNCTION_REGEX = new RegExp([
    "^\\s*function\\b",
    "\\(\\s*function",       // IIFE
    "\\(.*?\\)\\s*=>",       // arrow with args
    "(?:^|\\W)=>\\s*",       // bare arrow
    "\\bbind\\s*\\(",
    "\\bnew\\s+Function\\b"
  ].join("|"), "i");

  /**
   * Filters a node data object to remove ad/tracking, vendor blobs, and
   * irrelevant/huge attributes, returning a focused and clean result.
   */
  function FilterNodeData(nodeData, counters = { tooLargeAttrs: 0, truncatedAttrs: 0, skippedAttrs: 0 }) {
    const SKIPPABLE_DATA_ATTRS = [
      "data-img", "data-image", "data-icon", "data-thumbnail", "data-file", "data-payload", "data-base64", "data-binary",
      "data-pdf", "data-audio", "data-video", "data-csv", "data-json", "data-content", "data-template", "data-html",
      "data-fragment", "data-snippet", "data-body", "data-row-html", "data-render", "data-analytics", "data-tracking",
      "data-session", "data-context", "data-state", "data-config", "data-map", "data-chart", "data-graph", "data-stats",
      "data-user", "data-profile", "data-schema", "data-model", "data-row", "data-uid", "data-token", "data-sessionid",
      "data-fingerprint", "data-ga", "data-gtm", "data-amp", "data-ads", "data-ecid", "data-sentry", "data-bugsnag",
      "data-reactroot", "data-vue-meta", "data-hydrate", "data-django", "data-flask", "data-nextjs", "data-nuxt",
      "data-cid", "data-pid", "data-visitor", "data-test", "data-testid", "data-cy", "data-qa"
    ];

    const BLOCKED_FIELDS = [
      "eventTrackingBaseUrl", "customEventTrackingBaseUrl", "clickUrl", "thirdPartyUrls", "clickString",
      "activeViewUrlPrefix", "activeViewMetadata", "activeViewAttributes"
    ];

    const MAX_ATTR_LENGTH = 1000;
    const MAX_TRUNCATE_LENGTH = 100;

    function IsSkippableDataAttr(key) {
      return SKIPPABLE_DATA_ATTRS.some(skipKey => key.startsWith(skipKey));
    }

    function IsBlobString(val) {
      return (
        typeof val === "string" &&
        (
          val.length > MAX_ATTR_LENGTH ||
          /^data:image\//.test(val) ||
          /^https?:\/\/.{500,}/.test(val)
        )
      );
    }

    // Remove blocked fields from an object (shallow)
    function RemoveBlockedFields(obj) {
      if (!obj || typeof obj !== "object") return obj;
      for (const blocked of BLOCKED_FIELDS) {
        if (obj[blocked] !== undefined) delete obj[blocked];
      }
      return obj;
    }

    function StripJSFunctions(obj) {
      if (!obj || typeof obj !== "object") return obj;
      for (const [k, v] of Object.entries(obj)) {
        if (typeof v === "string" && JS_FUNCTION_REGEX.test(v.trim())) {
          obj[k] = null;
        } else if (typeof v === "object" && v !== null) {
          StripJSFunctions(v); // recursive for nested attrs
        }
      }
      return obj;
    }

    // Helper: Remove null/undefined/empty/"" values from object, always keep "tagName"
    function RemoveEmptyKeys(obj) {
      if (!obj || typeof obj !== "object") return obj;
      if (Array.isArray(obj)) {
        const filtered = obj.map(RemoveEmptyKeys).filter(x => x !== null && x !== undefined);
        return filtered.length > 0 ? filtered : undefined;
      }
      const result = {};
      for (const [k, v] of Object.entries(obj)) {
        if (k === "tagName") {
          // Always keep tagName, even if null
          result[k] = v;
          continue;
        }
        if (v === null || v === undefined || v === "") continue;
        const cleaned = RemoveEmptyKeys(v);
        if (cleaned !== undefined) result[k] = cleaned;
      }
      return result;
    }

    // --- Shallow clone to avoid mutating input ---
    const nd = { ...nodeData };

    // Strip JS code from all string fields and nested objects/attributes
    StripJSFunctions(nd);

    // Remove blocked fields at top level and in common subfields
    RemoveBlockedFields(nd);
    if (nd.attributes) RemoveBlockedFields(nd.attributes);
    if (nd.data_attrs) RemoveBlockedFields(nd.data_attrs);
    if (nd.aria_attrs) RemoveBlockedFields(nd.aria_attrs);

    // Data-* attributes filtering
    if (nd.data_attrs) {
      for (const key of Object.keys(nd.data_attrs)) {
        const val = nd.data_attrs[key];
        if (IsSkippableDataAttr(key) && IsBlobString(val)) {
          delete nd.data_attrs[key];
          counters.skippedAttrs++;
          continue;
        }
        if (typeof val === "string" && val.length > MAX_ATTR_LENGTH) {
          nd.data_attrs[key] = "[TOO_LARGE]";
          counters.tooLargeAttrs++;
        }
      }
      if (Object.keys(nd.data_attrs).length === 0) delete nd.data_attrs;
    }

    // Aria-* attributes: keep all, but flag blobs
    if (nd.aria_attrs) {
      for (const key of Object.keys(nd.aria_attrs)) {
        const val = nd.aria_attrs[key];
        if (typeof val === "string" && val.length > MAX_ATTR_LENGTH) {
          nd.aria_attrs[key] = "[TOO_LARGE]";
          counters.tooLargeAttrs++;
        }
      }
      if (Object.keys(nd.aria_attrs).length === 0) delete nd.aria_attrs;
    }

    // Generic attributes
    if (nd.attributes) {
      for (const [k, v] of Object.entries(nd.attributes)) {
        if (typeof v === "string" && v.length > MAX_ATTR_LENGTH) {
          nd.attributes[k] = v.slice(0, MAX_TRUNCATE_LENGTH) + "...[TRUNCATED]";
          counters.truncatedAttrs++;
        }
        if (IsSkippableDataAttr(k) && IsBlobString(v)) {
          delete nd.attributes[k];
          counters.skippedAttrs++;
        }
      }
      if (Object.keys(nd.attributes).length === 0) delete nd.attributes;
    }

    // Remove huge strings at top-level (except for text_content/title)
    for (const k of Object.keys(nd)) {
      if (
        typeof nd[k] === "string" &&
        nd[k].length > MAX_ATTR_LENGTH &&
        !["text_content", "title"].includes(k)
      ) {
        nd[k] = "[TOO_LARGE]";
        counters.tooLargeAttrs++;
      }
    }

    // At the end of FilterNodeData, before RemoveEmptyKeys or returning
    if (("calc_x" in nd) && ("calc_y" in nd) && ("calc_width" in nd) && ("calc_height" in nd) && ("view_x" in nd) && ("view_y" in nd)) {
      const allZeroOrNull =
        [nd.calc_x, nd.calc_y, nd.calc_width, nd.calc_height, nd.view_x, nd.view_y]
          .every(v => v === 0 || v === null || v === undefined);

      if (allZeroOrNull) {
        delete nd.calc_x;
        delete nd.calc_y;
        delete nd.calc_width;
        delete nd.calc_height;
        delete nd.view_x;
        delete nd.view_y;
      }
    }

    // Final clean-up: remove all null/empty/empty string keys (except "tagName")
    return RemoveEmptyKeys(nd);
  }

  const AD_IFRAME_DOMAINS = [
    // Google, DoubleClick, AdSense, GAM
    "doubleclick.net",
    "googlesyndication.com",
    "googletagservices.com",
    "googleadservices.com",
    "googleads.g.doubleclick.net",
    "adservice.google.com",

    // Taboola, Outbrain, RevContent, etc.
    "taboola.com",
    "outbrain.com",
    "revcontent.com",
    "zemanta.com",
    "mgid.com",

    // Amazon Ads
    "amazon-adsystem.com",
    "aaxads.com",

    // Yahoo/Bing/Microsoft
    "yahoo.com",
    "bing.com",
    "contextweb.com",

    // OpenX, AppNexus/Xandr, Criteo, Rubicon, Index, Pubmatic, etc.
    "adnxs.com",      // AppNexus/Xandr
    "criteo.com",
    "rubiconproject.com",
    "pubmatic.com",
    "openx.net",
    "indexexchange.com",
    "lkqd.net",
    "spotxchange.com",
    "sonobi.com",
    "teads.tv",

    // Adform, Adroll, Adzerk, Media.net, others
    "adform.net",
    "adroll.com",
    "adzerk.net",
    "media.net",
    "bidtheatre.com",
    "bidswitch.net",

    // Social and video ad networks
    "facebook.com",
    "fbcdn.net",
    "twitter.com",
    "snapads.com",
    "youtube.com",
    "tremorhub.com",
    "brightcove.com",

    // Misc. adserving
    "serving-sys.com",
    "moatads.com",
    "adblade.com",
    "yieldmo.com",
    "gumgum.com",
    "triplelift.com",
    "native.ai",
    "adentifi.com",
    "quantserve.com",
    "scorecardresearch.com"
  ];

  // Match with custom boundaries to catch hyphens/underscores/dots, etc.
  const B = String.raw`(?:^|[^a-z0-9])`;
  const E = String.raw`(?:$|[^a-z0-9])`;
  const AD_IFRAME_KEYWORD_RE = new RegExp(
    `${B}(ad|ads|advert|banner|sponsor|doubleclick|promoted|promotions|affiliate|paid|taboola|outbrain|amzn|yahoo|criteo|appnexus|xandr|revcontent|mgid|teads|pubmatic|rubicon|openx|bid|native|partner)${E}`,
    "i"
  );

  // Avoid obvious false positives
  const EXCLUDE_RE = /\b(address|adapter|adobe|shadow)\b/i;

  // Simple helper to detect if an iframe is a 3rd-party ad
  function IsAdIframe(node) {
    if (node.tagName && node.tagName.trim().toLowerCase() === "iframe") {
      const src = (node.src || "").toLowerCase();
      const id = (node.id || "").toLowerCase();
      const name = (node.name || "").toLowerCase();
      const cls = (node.getAttribute ? (node.getAttribute("class") || "") : "").toLowerCase();

      // 1. Domain match (safe for relative URLs)
      if (src) {
        try {
          const url = new URL(src, document.baseURI);
          if (AD_IFRAME_DOMAINS.some(dom => url.hostname.endsWith(dom))) {
            return true;
          }
        } catch (_) {} // ignore parse errors
      }

      // 2. Keyword in id/name/class/src
      const haystack = [src, id, name, cls].join(" ");
      if (AD_IFRAME_KEYWORD_RE.test(haystack) && !EXCLUDE_RE.test(haystack)) {
        return true;
      }

      // 3. Optional: data-ad* attributes
      const attrNames = typeof node.getAttributeNames === "function" ? node.getAttributeNames() : [];
      for (const attr of attrNames) {
        const a = attr.toLowerCase();
        if (a.startsWith("data-ad") || a.startsWith("data-adv") || a.startsWith("data-banner")) {
          return true;
        }
      }
    }
    return false;
  }

  function GetDirectText(node) {
    // Only text nodes directly inside this element (no recursion)
    let text = '';
    for (const child of node.childNodes) {
      if (child.nodeType === Node.TEXT_NODE) {
        text += child.textContent;
      }
    }
    // Collapse multiple whitespace into a single space, trim ends
    return text.replace(/\s+/g, ' ').trim();
  }

  function GetInclusiveText(innerText, textContent) {
    // Use innerText if available (matches what the user visually sees)
    if (typeof innerText === 'string') {
      return innerText.replace(/\s+/g, ' ').trim();
    } else if (typeof textContent === 'string') {
      // Fallback for SVG or non-HTML elements
      return textContent.replace(/\s+/g, ' ').trim();
    }
    return '';
  }

  /**
   * Creates a node data object for a given node and its descendants.
   *
   * @param {HTMLElement} node - The node to process.
   * @param {HTMLElement | null} parentIframe - The parent iframe node.
   * @returns {string | null} The ID of the node data object, or null if the node is not processed.
   */
  function BuildDomTree(node, counters, parentIframe = null) {
    // Reject early: only ELEMENT or TEXT nodes, never highlight overlay, never null
    if (!node || node.id === HIGHLIGHT_CONTAINER_ID ||
      (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE)) {
      return null;
    }

    const SKIP_TAGS = new Set([
      "base", "embed", "head", "link", "meta", "noscript", "object", "param", "script", "slot", "source", "style",
      "template", "title", "track"
    ]);
    if (node.tagName && SKIP_TAGS.has(node.tagName.trim().toLowerCase())) { return null; }

    if (IsAdIframe(node)) {
      counters.adIframes = (counters.adIframes || 0) + 1;
      return null;
    }

    // Special case: root body node
    /*
    if (node === document.body) {
      const xpathData = GenerateXPathData(node);
      const nodeData = {
        tagName: 'body',
        attributes: {},
        children: [],
        is_really_visible: IsElementReallyVisible(node),
        xpath_local: xpathData.xpath_local,
        frame_chain: xpathData.frame_chain,
        cross_origin_frame: xpathData.cross_origin_frame,
        shadow_host_xpath: xpathData.shadow_host_xpath,
        shadow_internal_path: xpathData.shadow_internal_path
      };
      for (const child of node.childNodes) {
        const domElement = BuildDomTree(child, counters, parentIframe);
        if (domElement) nodeData.children.push(domElement);
      }
      const id = `${ID.current++}`;
      DOM_HASH_MAP[id] = nodeData;
      return id;
    }
    */

    // TEXT NODE: attach only visible, non-empty
    if (node.nodeType === Node.TEXT_NODE) {
      const textContent = node.textContent;
      if (!textContent?.trim()) return null;
      if (JS_FUNCTION_REGEX.test(textContent.trim())) return null;
      const parentElement = node.parentElement;
      if (!parentElement || parentElement.tagName.trim().toLowerCase() === 'script') return null;
      const id = `${ID.current++}`;
      const cleanText = textContent.replace(/\s+/g, ' ').trim();
      DOM_HASH_MAP[id] = {
        tagName: "#text",
        direct_text: cleanText,
        inclusive_text: cleanText,
        text_content: textContent,
        is_really_visible: IsTextNodeVisible(node)
      };
      return id;
    }

    // ELEMENT NODE: filter by acceptance
    if (!IsElementAccepted(node) && node !== document.body) return null;

    // Optional: viewport quick rejection (same as your version)
    if (typeof viewportExpansion !== "undefined" && viewportExpansion !== -1 && !node.shadowRoot && node !== document.body) {
      const rect = DOM_CACHE.GetBoundingRect(node);
      const style = DOM_CACHE.GetComputedStyle(node);
      const isFixedOrSticky = style && (style.position === 'fixed' || style.position === 'sticky');
      const hasSize = node.offsetWidth > 0 || node.offsetHeight > 0;
      const vw = node.ownerDocument?.defaultView || window;
      if (!rect || (!isFixedOrSticky && !hasSize && (
        rect.bottom < -viewportExpansion ||
        rect.top > vw.innerHeight + viewportExpansion ||
        rect.right < -viewportExpansion ||
        rect.left > vw.innerWidth + viewportExpansion
      ))) {
        return null;
      }
    }

    // Gather all metadata fields (flat view + more)
    const tagName = node.tagName.trim().toLowerCase();
    const rect = DOM_CACHE.GetBoundingRect(node);
    const style = DOM_CACHE.GetComputedStyle(node);
    const xpathData = GenerateXPathData(node);
    const isReallyVisible = IsElementReallyVisible(node);
    const innerText = node.innerText || null;
    const textContent = node.textContent || null;
    const isInteractive = IsInteractiveElement(node);

    // aria-* and data-* attributes
    const aria_attrs = {};
    const data_attrs = {};
    for (const attr of node.attributes) {
      if (attr.name.startsWith('aria-')) aria_attrs[attr.name] = attr.value;
      if (attr.name.startsWith('data-')) data_attrs[attr.name] = attr.value;
    }

    // Core metadata for every element node
    const nodeData = {
      tagName,
      id: node.id || null,
      class: node.getAttribute ? (node.getAttribute('class') || null) : null,
      type: node.type || null,
      name: node.name || null,
      role: node.getAttribute('role') || null,
      //outerHTML: node.outerHTML,  // enable for debugging
      text_content: isReallyVisible ? innerText : null,
      direct_text: GetDirectText(node),
      inclusive_text: GetInclusiveText(innerText, textContent),
      value: node.value !== undefined ? node.value : null,
      href: node.href !== undefined ? node.href : null,
      title: node.title || null,
      attributes: {},
      aria_attrs,
      data_attrs,
      calc_x: rect?.x ?? null,
      calc_y: rect?.y ?? null,
      calc_width: rect?.width ?? null,
      calc_height: rect?.height ?? null,
      view_x: rect?.left ?? null,
      view_y: rect?.top ?? null,
      computed_display: style?.display ?? null,
      computed_visibility: style?.visibility ?? null,
      computed_opacity: style?.opacity ?? null,
      computed_pointer_events: style?.pointerEvents ?? null,
      is_really_visible: isReallyVisible,
      is_subtree_visible: IsSubtreeVisible(node),
      is_interactive: isInteractive,
      is_distinct_interaction: IsElementDistinctInteraction(node, isInteractive),
      is_in_viewport: IsInExpandedViewport(node, viewportExpansion),
      is_top_element: IsTopElement(node),
      elem_index: typeof node.dataset?.elemIndex !== 'undefined' ? node.dataset.elemIndex : null,
      children: [],
      xpath_local: xpathData.xpath_local,
      frame_chain: xpathData.frame_chain,
      cross_origin_frame: xpathData.cross_origin_frame,
      shadow_host_xpath: xpathData.shadow_host_xpath,
      shadow_internal_path: xpathData.shadow_internal_path
    };

    // Optionally attach all DOM attributes (if you want "attributes" field like before)
    const skipAttr = new Set(["id", "class", "name", "role", "type", "href", "title", "value"]);
    //if (nodeData.is_interactive || tagName === 'iframe' || tagName === 'body') {
      const attributeNames = node.getAttributeNames?.() || [];
      for (const name of attributeNames) {
        if (skipAttr.has(name) || name.startsWith("on")) { continue; }
        const value = node.getAttribute(name);
        nodeData.attributes[name] = value;
      }
    //}

    // Process children (iframe, contenteditable, shadow DOM, normal)
    if (node.tagName) {
      // --- iframe ---
      if (tagName === "iframe") {
        try {
          const win = node.contentWindow;
          let sameOrigin = false;

          // Double-check same-origin before touching the iframe document
          try {
            sameOrigin = IsSameOrigin(win);
          } catch {
            sameOrigin = false;
          }

          if (sameOrigin) {
            const iframeDoc = win?.document || node.contentDocument;
            if (iframeDoc) {
              for (const child of iframeDoc.childNodes) {
                const domElement = BuildDomTree(child, counters, node);
                if (domElement) nodeData.children.push(domElement);
              }
            }
          } else {
            nodeData.cross_origin_frame = true;
            // Do not traverse — record metadata only
          }
        } catch (e) {
          console.warn("Unable to access iframe:", e);
          nodeData.cross_origin_frame = true;
        }
      }
      // --- rich text/contenteditable ---
      else if (
        node.isContentEditable ||
        node.getAttribute("contenteditable") === "true" ||
        node.id === "tinymce" ||
        (node.classList && node.classList.contains("mce-content-body")) ||
        (tagName === "body" && node.getAttribute("data-id")?.startsWith("mce_"))
      ) {
        for (const child of node.childNodes) {
          const domElement = BuildDomTree(child, counters, parentIframe);
          if (domElement) nodeData.children.push(domElement);
        }
      }
      // --- shadow DOM ---
      else if (node.shadowRoot) {
        nodeData.shadowRoot = true;
        for (const child of node.shadowRoot.childNodes) {
          const domElement = BuildDomTree(child, counters, parentIframe);
          if (domElement) nodeData.children.push(domElement);
        }
        for (const child of node.childNodes) {
          const domElement = BuildDomTree(child, counters, parentIframe);
          if (domElement) nodeData.children.push(domElement);
        }
      }
      // --- normal children ---
      else {
        for (const child of node.childNodes) {
          const domElement = BuildDomTree(child, counters, parentIframe);
          if (domElement) nodeData.children.push(domElement);
        }
      }
    }

    // Special case: strip data from body nodeData
    if (node === document.body) {
      nodeData.text_content = null;
      nodeData.inclusive_text = null;
      nodeData.direct_text = null;
    }

    // Special skip: empty anchors (same as before)
    if (nodeData.tagName === 'a' && nodeData.children.length === 0 && !nodeData.href) {
      const hasSize = (rect && rect.width > 0 && rect.height > 0) || (node.offsetWidth > 0 || node.offsetHeight > 0);
      if (!hasSize) return null;
    }

    const id = `${ID.current++}`;
    DOM_HASH_MAP[id] = FilterNodeData(nodeData, counters);
    return id;
  }

  const counters = { tooLargeAttrs: 0, truncatedAttrs: 0, skippedAttrs: 0, adIframes: 0 };
  const rootId = BuildDomTree(document.body, counters);

  const html = document.documentElement;
  const doc_lang = html.getAttribute('lang') || null;
  const doc_dir = html.getAttribute('dir') || null;
  const page_text =
    typeof document.body?.innerText === 'string'
      ? document.body.innerText.replace(/\s+/g, ' ').trim()
      : '';
  const snapshot_meta = {
    url: location.href,
    base_uri: document.baseURI || location.href,
    ts_ms: Date.now()
  };
  const viewport = { height: window.innerHeight, scrollX: window.scrollX, scrollY: window.scrollY, width: window.innerWidth };
  const document_dims = { height: document.documentElement.scrollHeight, width: document.documentElement.scrollWidth };

  DOM_CACHE.ClearCache()

  return {
    rootId,
    map: DOM_HASH_MAP,
    counters,
    doc_dir,
    doc_lang,
    page_text,
    snapshot_meta,
    viewport,
    document_dims
  };
};
