/**
 * Selection chrome (floating border + ×) for BM element selection.
 * Shared by Gutenberg canvas, admin preview iframe, and frontend editor.
 *
 * Chrome is positioned on the preview root (not inside the element) so
 * overflow:hidden slides / full-width stacks do not clip or look like a
 * container border.
 */

import { endPreviewContentEditForLinkEditor } from './previewContentEdit';

/**
 * Spacing belongs on the [data-bm-el] host (CSS from cssVars). Strip it from
 * media nodes so hit-wrappers do not stack the same margin/padding twice.
 *
 * @param {HTMLElement} node
 */
export function stripMediaOuterSpacing( node ) {
	if ( ! node?.style ) {
		return;
	}
	node.style.marginTop = '';
	node.style.marginBottom = '';
	node.style.paddingTop = '';
	node.style.paddingRight = '';
	node.style.paddingBottom = '';
	node.style.paddingLeft = '';
}

/**
 * Keys that own replaced media (image / video / embed). Nested imgs under
 * logos/buttons must not inherit those keys or media-align margins.
 *
 * @param {string} key
 * @return {boolean}
 */
function isMediaElementKey( key ) {
	const k = String( key || '' )
		.trim()
		.toLowerCase();
	return k === 'image' || k === 'media' || k === 'video';
}

/**
 * Nested hosts that should own the selection chrome instead of the card shell.
 * (Quote / feature icons and avatars need a visible active ring when clicked.)
 *
 * @param {string} key
 * @return {boolean}
 */
function prefersNestedSelectionChrome( key ) {
	const k = String( key || '' )
		.trim()
		.toLowerCase();
	return k === 'icon' || k === 'accordionarrow' || isMediaElementKey( k );
}

/**
 * Resolve the styleable media key for a node, if it should get a hit wrap.
 *
 * @param {Element} node
 * @return {string}
 */
function mediaHitKeyForNode( node ) {
	if ( ! node ) {
		return '';
	}
	// Logo row imgs are decorative children of [data-bm-el="logos"].
	if (
		node.classList?.contains( 'bm-hero__logo' ) ||
		node.closest?.( '.bm-hero__logos, [data-bm-el="logos"]' )
	) {
		return '';
	}
	const own = String( node.getAttribute( 'data-bm-el' ) || '' ).trim();
	if ( own ) {
		return isMediaElementKey( own ) ? own : '';
	}
	const host = node.closest?.( '[data-bm-el]' );
	const hostKey = String( host?.getAttribute?.( 'data-bm-el' ) || '' ).trim();
	return isMediaElementKey( hostKey ) ? hostKey : '';
}

/**
 * Circular avatars must not use the full-bleed `.bm-media-frame` (width 100%).
 *
 * @param {Element} node
 * @return {boolean}
 */
function isCompactAvatarMedia( node ) {
	return Boolean( node?.classList?.contains( 'bm-testimonial__avatar' ) );
}

/**
 * Full-bleed / section background media must stay pointer-events:none with no
 * hit overlay — an inset:0 overlay on a fill video blocks every other element.
 *
 * @param {Element} node
 * @return {boolean}
 */
function isCoverBackgroundMedia( node ) {
	if ( ! node?.matches?.( 'video, iframe, embed, object' ) ) {
		return false;
	}
	if ( node.classList?.contains( 'bm-hero__video' ) ) {
		return true;
	}
	if ( node.closest?.( '.bm-hero--video, [data-layout="video-background"]' ) ) {
		return true;
	}
	const style = String( node.getAttribute( 'style' ) || '' );
	return (
		/position\s*:\s*absolute/i.test( style ) &&
		(/inset\s*:\s*0\b/i.test( style ) ||
			(/top\s*:\s*0/i.test( style ) &&
				/left\s*:\s*0/i.test( style ) &&
				/width\s*:\s*100%/i.test( style ) &&
				/height\s*:\s*100%/i.test( style )))
	);
}

/**
 * @param {HTMLElement} host
 */
function ensureHitPositioning( host ) {
	if ( ! ( host instanceof HTMLElement ) ) {
		return;
	}
	// Inline + important so layout !important rules cannot leave the overlay
	// positioned against .bm-hero__grid / section (which would cover content).
	host.style.setProperty( 'position', 'relative', 'important' );
}

/**
 * @param {HTMLElement} host
 */
function ensureHitOverlay( host ) {
	if ( ! ( host instanceof HTMLElement ) ) {
		return;
	}
	ensureHitPositioning( host );
	let overlay = null;
	try {
		overlay = host.querySelector( ':scope > .bm-el-hit__overlay' );
	} catch ( e ) {
		overlay = Array.from( host.children ).find( ( child ) =>
			child.classList?.contains( 'bm-el-hit__overlay' )
		);
	}
	if ( ! overlay ) {
		overlay = ( host.ownerDocument || document ).createElement( 'span' );
		overlay.className = 'bm-el-hit__overlay';
		overlay.setAttribute( 'aria-hidden', 'true' );
		host.appendChild( overlay );
	}
	overlay.style.cssText =
		'position:absolute;inset:0;z-index:2;cursor:pointer;background:transparent;display:block';
}

/**
 * @param {HTMLElement} wrap
 */
function unwrapHitTarget( wrap ) {
	const parent = wrap?.parentNode;
	if ( ! parent ) {
		return;
	}
	Array.from( wrap.children )
		.filter( ( child ) => child.classList?.contains( 'bm-el-hit__overlay' ) )
		.forEach( ( el ) => el.remove() );
	while ( wrap.firstChild ) {
		parent.insertBefore( wrap.firstChild, wrap );
	}
	wrap.remove();
}

/**
 * Wrap replaced media so clicks reach styleable targets.
 * iframes/videos swallow events; images can start a drag instead of a click.
 * Prefers the permanent `.bm-media-frame` from mediaToHtml when present.
 *
 * @param {Element} node
 * @param {string} key
 * @return {Element}
 */
export function ensureMediaHitTarget( node, key ) {
	if ( ! node?.matches?.( 'iframe, video, embed, object, img' ) ) {
		return node;
	}

	const mediaKey = isMediaElementKey( key ) ? key : '';
	const compactAvatar = isCompactAvatarMedia( node );
	const applyMediaMargins = Boolean( mediaKey ) && ! compactAvatar;

	// Cover / background video: never add a capturing overlay.
	if ( isCoverBackgroundMedia( node ) ) {
		const badWrap = node.closest?.( '.bm-el-hit' );
		if (
			badWrap instanceof HTMLElement &&
			badWrap !== node &&
			! badWrap.classList.contains( 'bm-hero__media' ) &&
			Array.from( badWrap.children ).includes( node )
		) {
			const keyKeep =
				badWrap.getAttribute( 'data-bm-el' ) || mediaKey || '';
			unwrapHitTarget( badWrap );
			if ( keyKeep ) {
				node.setAttribute( 'data-bm-el', keyKeep );
			}
		}
		node.style.pointerEvents = 'none';
		node.setAttribute( 'draggable', 'false' );
		node.removeAttribute( 'data-bm-selected' );
		if ( mediaKey ) {
			node.setAttribute( 'data-bm-el', mediaKey );
			node.setAttribute( 'data-bm-styleable', 'true' );
		}
		return node;
	}

	// Prefer permanent .bm-media-frame (oEmbed wraps iframe deeper than one level).
	const frame = node.closest?.( '.bm-media-frame' );
	if ( frame instanceof HTMLElement ) {
		// Avatar must not keep a full-bleed media frame.
		if ( compactAvatar ) {
			const keyKeep =
				frame.getAttribute( 'data-bm-el' ) || mediaKey || '';
			unwrapHitTarget( frame );
			if ( keyKeep ) {
				node.setAttribute( 'data-bm-el', keyKeep );
			}
			return ensureMediaHitTarget( node, keyKeep || key );
		}
		frame.classList.add( 'bm-el-hit' );
		if ( applyMediaMargins ) {
			frame.classList.add( 'bm-el-hit--media' );
			if ( ! frame.style.marginLeft ) {
				frame.style.marginLeft =
					'var(--bm-mf-media-margin-start, var(--bm-media-margin-inline-start, 0))';
			}
			if ( ! frame.style.marginRight ) {
				frame.style.marginRight =
					'var(--bm-mf-media-margin-end, var(--bm-media-margin-inline-end, 0))';
			}
		}
		ensureHitPositioning( frame );
		if ( mediaKey ) {
			frame.setAttribute( 'data-bm-el', mediaKey );
			frame.setAttribute( 'data-bm-styleable', 'true' );
			node.removeAttribute( 'data-bm-el' );
			node.removeAttribute( 'data-bm-styleable' );
		}
		node.removeAttribute( 'data-bm-selected' );
		node.setAttribute( 'draggable', 'false' );
		stripMediaOuterSpacing( node );
		if ( applyMediaMargins ) {
			node.style.marginLeft = '0';
			node.style.marginRight = '0';
		}
		if ( node.matches( 'iframe, video, embed, object' ) ) {
			node.style.pointerEvents = 'none';
			ensureHitOverlay( frame );
		}
		return frame;
	}

	let wrap = node.parentElement;
	const parentIsFrame = wrap?.classList?.contains( 'bm-media-frame' );
	const parentIsHit = wrap?.classList?.contains( 'bm-el-hit' );

	if ( ! parentIsFrame && ! parentIsHit ) {
		wrap = document.createElement( 'div' );
		wrap.className = applyMediaMargins
			? 'bm-media-frame bm-el-hit bm-el-hit--media'
			: 'bm-el-hit';
		wrap.style.cssText = applyMediaMargins
			? 'position:relative;display:block;width:var(--bm-mf-img-width, var(--bm-image-width, 100%));max-width:100%;' +
			  'margin-left:var(--bm-mf-media-margin-start, var(--bm-media-margin-inline-start, 0));' +
			  'margin-right:var(--bm-mf-media-margin-end, var(--bm-media-margin-inline-end, 0));' +
			  'box-sizing:border-box;line-height:0'
			: compactAvatar
			? 'position:relative;display:block;width:fit-content;flex-shrink:0;max-width:100%;box-sizing:border-box;line-height:0;order:var(--bm-author-image-order, 0)'
			: 'position:relative;display:block;width:fit-content;max-width:100%;' +
			  'box-sizing:border-box;line-height:0';
		node.parentNode.insertBefore( wrap, node );
		wrap.appendChild( node );
	} else if ( wrap instanceof HTMLElement ) {
		wrap.classList.add( 'bm-el-hit' );
		if ( applyMediaMargins ) {
			wrap.classList.add( 'bm-media-frame', 'bm-el-hit--media' );
			if ( ! wrap.style.marginLeft ) {
				wrap.style.marginLeft =
					'var(--bm-mf-media-margin-start, var(--bm-media-margin-inline-start, 0))';
			}
			if ( ! wrap.style.marginRight ) {
				wrap.style.marginRight =
					'var(--bm-mf-media-margin-end, var(--bm-media-margin-inline-end, 0))';
			}
		} else {
			wrap.classList.remove( 'bm-el-hit--media', 'bm-media-frame' );
			wrap.style.marginLeft = '';
			wrap.style.marginRight = '';
			if ( compactAvatar ) {
				wrap.style.width = 'fit-content';
				wrap.style.flexShrink = '0';
				wrap.style.maxWidth = '100%';
				wrap.style.order = 'var(--bm-author-image-order, 0)';
			}
		}
		ensureHitPositioning( wrap );
	}

	if ( mediaKey ) {
		wrap.setAttribute( 'data-bm-el', mediaKey );
		wrap.setAttribute( 'data-bm-styleable', 'true' );
		node.removeAttribute( 'data-bm-el' );
		node.removeAttribute( 'data-bm-styleable' );
	}
	node.removeAttribute( 'data-bm-selected' );
	node.setAttribute( 'draggable', 'false' );
	stripMediaOuterSpacing( node );
	if ( node instanceof HTMLElement && applyMediaMargins ) {
		node.style.marginLeft = '0';
		node.style.marginRight = '0';
	}

	if ( node.matches( 'iframe, video, embed, object' ) ) {
		node.style.pointerEvents = 'none';
		ensureHitOverlay( wrap );
	}

	return wrap;
}

/**
 * Wrap all styleable media nodes inside a BM root.
 *
 * @param {HTMLElement|null} root
 * @param {string[]|Set<string>|null} [selectableKeys] Optional filter; omit to wrap any media with data-bm-el.
 */
export function wrapMediaHitTargets( root, selectableKeys = null ) {
	if ( ! root ) {
		return;
	}
	const set =
		selectableKeys == null
			? null
			: selectableKeys instanceof Set
			? selectableKeys
			: new Set( ( selectableKeys || [] ).filter( Boolean ) );

	// Undo earlier mistaken wraps around logo row images (media margins / stolen key).
	root
		.querySelectorAll(
			'.bm-hero__logos > .bm-el-hit, .bm-el-hit[data-bm-el="logos"]'
		)
		.forEach( ( wrap ) => {
			unwrapHitTarget( wrap );
		} );

	// Undo full-bleed media frames around testimonial avatars (width:100%).
	root
		.querySelectorAll(
			'.bm-testimonial__meta > .bm-media-frame, .bm-testimonial__meta > .bm-el-hit--media'
		)
		.forEach( ( wrap ) => {
			const img = wrap.querySelector( 'img.bm-testimonial__avatar' );
			const keyKeep = wrap.getAttribute( 'data-bm-el' ) || 'image';
			unwrapHitTarget( wrap );
			if ( img && keyKeep ) {
				img.setAttribute( 'data-bm-el', keyKeep );
			}
		} );

	// Undo hit wraps around cover/background videos (overlay blocked other elements).
	root
		.querySelectorAll(
			'.bm-hero--video > .bm-el-hit, [data-layout="video-background"] > .bm-el-hit'
		)
		.forEach( ( wrap ) => {
			const media = wrap.querySelector( 'video, iframe, embed, object' );
			const keyKeep = wrap.getAttribute( 'data-bm-el' ) || '';
			unwrapHitTarget( wrap );
			if ( media && keyKeep ) {
				media.setAttribute( 'data-bm-el', keyKeep );
				media.style.pointerEvents = 'none';
			}
		} );

	const mediaNodes = Array.from(
		root.querySelectorAll( 'iframe, video, embed, object, img' )
	).filter( ( node ) => {
		if ( node.closest( '.bm-el-chrome' ) ) {
			return false;
		}
		const key = mediaHitKeyForNode( node );
		if ( ! key ) {
			return false;
		}
		return ! set || set.has( key );
	} );

	mediaNodes.forEach( ( node ) => {
		const key = mediaHitKeyForNode( node );
		if ( key ) {
			ensureMediaHitTarget( node, key );
		}
	} );
}

/**
 * Card index from a preview node (data-bm-card-index).
 *
 * @param {Element|null} el
 * @return {number|null}
 */
export function cardIndexFromElement( el ) {
	if ( ! el || typeof el.closest !== 'function' ) {
		return null;
	}
	const marked = el.closest( '[data-bm-card-index]' );
	if ( marked ) {
		const n = Number( marked.getAttribute( 'data-bm-card-index' ) );
		if ( Number.isFinite( n ) && n >= 0 ) {
			return n;
		}
	}
	return null;
}

/**
 * Features card shell (article.bm-card) for the focused card index.
 *
 * @param {ParentNode|null} root
 * @param {number|null}     cardIndex
 * @return {Element|null}
 */
export function resolveCardShellHost( root, cardIndex ) {
	if ( ! root || typeof root.querySelector !== 'function' ) {
		return null;
	}
	// Number(null) === 0 — treat unset focus as no card, not the first card.
	if ( cardIndex == null || cardIndex === '' ) {
		return null;
	}
	const n = Number( cardIndex );
	if ( ! Number.isFinite( n ) || n < 0 ) {
		return null;
	}
	return (
		root.querySelector( `.bm-card[data-bm-card-index="${ n }"]` ) ||
		root.querySelector( `.bm-pricing__card[data-bm-card-index="${ n }"]` ) ||
		root.querySelector( `.bm-testimonial[data-bm-card-index="${ n }"]` ) ||
		null
	);
}

/**
 * Open accordion `<details>` for editing so nested content is reachable.
 * Honors Initially open mode (`first` / `all` / `none`). With a focused card,
 * that item stays open for editing unless mode is `none` and no focus is set.
 * Does not trap toggles.
 *
 * @param {ParentNode|null} root Document or element.
 * @param {{ focusCardIndex?: number|null, openMode?: 'first'|'all'|'none' }} [options]
 */
export function forceOpenDetailsForEditing( root, options = {} ) {
	if ( ! root || typeof root.querySelectorAll !== 'function' ) {
		return;
	}
	// Document-wide calls (admin iframe / mistaken scope) must not toggle every
	// accordion on the page — prefer the selected block or a single preview root.
	let scope = root;
	if ( root.nodeType === 9 ) {
		const selected =
			root.querySelector(
				'.wp-block.is-selected .bm-accordion, .block-editor-block-list__block.is-selected .bm-accordion, .is-selected .bm-accordion'
			) || null;
		const preview =
			root.querySelector( '.bm-preview-root .bm-accordion' ) ||
			root.querySelector( '.bm-preview-root' ) ||
			root.querySelector( '.bm-accordion' );
		scope = selected || preview || root;
	}

	const detailsList = scope.querySelectorAll( 'details.bm-accordion__item' );
	if ( ! detailsList.length ) {
		return;
	}

	const mode =
		options.openMode === 'all' ||
		options.openMode === 'first' ||
		options.openMode === 'none'
			? options.openMode
			: accordionOpenModeFromRoot( scope );

	const focusRaw = options.focusCardIndex;
	const hasFocus =
		focusRaw != null && Number.isFinite( Number( focusRaw ) );
	const focusIndex = hasFocus ? Number( focusRaw ) : 0;

	if ( mode === 'all' ) {
		detailsList.forEach( ( details ) => {
			details.removeAttribute( 'data-bm-preview-open' );
			details.open = true;
		} );
		return;
	}

	if ( mode === 'none' && ! hasFocus ) {
		detailsList.forEach( ( details ) => {
			details.removeAttribute( 'data-bm-preview-open' );
			details.open = false;
		} );
		return;
	}

	detailsList.forEach( ( details ) => {
		details.removeAttribute( 'data-bm-preview-open' );
		const idx = Number( details.getAttribute( 'data-bm-card-index' ) );
		const hasIndex = Number.isFinite( idx );
		const shouldOpen = hasFocus
			? hasIndex
				? idx === focusIndex
				: false
			: hasIndex
			? idx === 0
			: true;
		if ( shouldOpen ) {
			details.open = true;
		} else {
			details.open = false;
		}
	} );
}

/**
 * @param {ParentNode|null} root
 * @return {'first'|'all'|'none'}
 */
function accordionOpenModeFromRoot( root ) {
	if ( ! root || typeof getComputedStyle !== 'function' ) {
		return 'first';
	}
	const start =
		( typeof root.querySelector === 'function' &&
			root.querySelector( '.bm-accordion' ) ) ||
		( root.nodeType === 1 && root.matches?.( '.bm-accordion' )
			? root
			: null ) ||
		( root.nodeType === 1 ? root : root.body || null );
	let el = start;
	while ( el && el.nodeType === 1 ) {
		const raw = getComputedStyle( el )
			.getPropertyValue( '--bm-accordion-open-default' )
			.trim()
			.toLowerCase();
		if ( raw ) {
			if (
				raw === 'all' ||
				raw === '1' ||
				raw === 'true' ||
				raw === 'yes' ||
				raw === 'on'
			) {
				return 'all';
			}
			if (
				raw === 'none' ||
				raw === '0' ||
				raw === 'false' ||
				raw === 'off' ||
				raw === 'closed'
			) {
				return 'none';
			}
			return 'first';
		}
		el = el.parentElement;
	}
	return 'first';
}

/**
 * @deprecated No longer traps accordion toggles. Kept for call-site compat.
 *
 * @param {Document|null} doc
 * @return {Function|undefined} Cleanup.
 */
export function bindForceOpenDetails( doc ) {
	void doc;
	return undefined;
}

/**
 * Check the radio for a tabs label so its panel is visible while editing.
 * Preview click handlers call preventDefault, which blocks native label→radio.
 *
 * @param {Element|null} el       Hit target (label, panel, or child).
 * @param {number|null}  [index]  Optional card index when el is a root/container.
 */
export function activateBmTabForEditing( el, index = null ) {
	if ( ! el || typeof el.querySelector !== 'function' ) {
		return;
	}

	const scope =
		typeof el.closest === 'function' ? el.closest( '.bm-tabs' ) : null;
	const root = scope || ( el.querySelector?.( '.bm-tabs' ) ? el : null );
	if ( ! root ) {
		return;
	}

	let label = null;
	if ( typeof el.closest === 'function' ) {
		label =
			el.closest( '.bm-tabs__label' ) ||
			( el.matches?.( '[data-bm-el="tabTitle"]' ) ? el : null );
		if ( ! label ) {
			const panel =
				el.closest( '.bm-tabs__panel' ) ||
				( el.matches?.( '[data-bm-el="tabContent"]' ) ? el : null );
			if ( panel ) {
				let prev = panel.previousElementSibling;
				while (
					prev &&
					! prev.classList?.contains( 'bm-tabs__label' )
				) {
					prev = prev.previousElementSibling;
				}
				label = prev;
			}
		}
	}

	const cardIndex =
		index != null && Number.isFinite( Number( index ) )
			? Number( index )
			: cardIndexFromElement( el );
	if ( ! label && cardIndex != null ) {
		label = root.querySelector(
			`.bm-tabs__label[data-bm-card-index="${ cardIndex }"]`
		);
	}
	if ( ! label ) {
		return;
	}

	const forId = label.getAttribute( 'for' );
	let radio = null;
	// Prefer the adjacent radio inside this tabs root — never document.getElementById,
	// which returns the first duplicate id when sibling blocks share content.
	const prev = label.previousElementSibling;
	if ( prev?.classList?.contains( 'bm-tabs__input' ) && root.contains( prev ) ) {
		radio = prev;
	}
	if ( ! radio && forId ) {
		try {
			const scoped = root.querySelector( `#${ CSS.escape( forId ) }` );
			if ( scoped?.classList?.contains( 'bm-tabs__input' ) ) {
				radio = scoped;
			}
		} catch ( e ) {
			// ignore invalid selectors
		}
	}
	if ( ! radio?.classList?.contains( 'bm-tabs__input' ) ) {
		radio = null;
	}
	if ( radio && radio.type === 'radio' && root.contains( radio ) ) {
		radio.checked = true;
	}
}

/**
 * Slide index from a preview node (data-bm-slide-index or Splide slide).
 *
 * @param {Element|null} el
 * @return {number|null}
 */
export function slideIndexFromElement( el ) {
	if ( ! el || typeof el.closest !== 'function' ) {
		return null;
	}
	const marked = el.closest( '[data-bm-slide-index]' );
	if ( marked ) {
		const n = Number( marked.getAttribute( 'data-bm-slide-index' ) );
		if ( Number.isFinite( n ) && n >= 0 ) {
			return n;
		}
	}

	const slide = el.closest( '.splide__slide' );
	if ( ! slide ) {
		return null;
	}

	// Splide clone → map back to real slide via matching data-bm-slide-index / content.
	const root = slide.closest( '[data-bm-slider]' );
	const instance = root?._bmSplide;
	if ( instance && typeof instance.index === 'number' ) {
		// Click is almost always on the active/visible slide while editing.
		if (
			slide.classList.contains( 'is-active' ) ||
			slide.classList.contains( 'is-visible' )
		) {
			return instance.index;
		}
	}

	const siblings = Array.from( slide.parentElement?.children || [] ).filter(
		( node ) =>
			node.classList?.contains( 'splide__slide' ) &&
			! node.classList.contains( 'splide__slide--clone' )
	);
	const idx = siblings.indexOf( slide );
	if ( idx >= 0 ) {
		return idx;
	}

	// Clone without is-active: find original with same slide index attr inside.
	const cloneMarked = slide.querySelector?.( '[data-bm-slide-index]' );
	if ( cloneMarked ) {
		const n = Number( cloneMarked.getAttribute( 'data-bm-slide-index' ) );
		if ( Number.isFinite( n ) && n >= 0 ) {
			return n;
		}
	}

	if ( instance && typeof instance.index === 'number' ) {
		return instance.index;
	}

	return null;
}

/**
 * Whether a click target is slider chrome (arrows / dots) — not slide content.
 *
 * @param {Element|null} el
 * @return {boolean}
 */
export function isSliderChromeTarget( el ) {
	if ( ! el || typeof el.closest !== 'function' ) {
		return false;
	}
	return Boolean(
		el.closest(
			'.splide__arrow, .splide__pagination, .splide__toggle, .bm-el-deselect, .bm-el-chrome, .bm-tabs__add, [data-bm-add-tab]'
		)
	);
}

/**
 * Whether a node is inside a visible / active Splide slide (or not in a slider).
 *
 * @param {Element} node
 * @return {boolean}
 */
function isInActiveOrStaticSlide( node ) {
	const slide = node.closest?.( '.splide__slide' );
	if ( ! slide ) {
		return true;
	}
	if ( slide.classList.contains( 'splide__slide--clone' ) ) {
		return false;
	}
	if (
		slide.classList.contains( 'is-active' ) ||
		slide.classList.contains( 'is-visible' )
	) {
		return true;
	}
	// Before Splide init, all slides are fair game.
	const list = slide.closest( '.splide' );
	return ! list?.classList?.contains( 'is-initialized' );
}

/**
 * Pick the best [data-bm-el] node for a key (prefer focused / active slide).
 *
 * @param {HTMLElement} root
 * @param {string} key
 * @param {number|null} [preferredSlideIndex]
 * @param {number|null} [preferredCardIndex]
 * @return {Element|null}
 */
export function resolveStyleableElement(
	root,
	key,
	preferredSlideIndex = null,
	preferredCardIndex = null
) {
	if ( ! root || ! key ) {
		return null;
	}
	const nodes = Array.from(
		root.querySelectorAll( `[data-bm-el="${ CSS.escape( key ) }"]` )
	).filter(
		( node ) =>
			! node.classList?.contains( 'bm-el-chrome' ) &&
			! node.classList?.contains( 'bm-el-chrome--float' )
	);
	if ( ! nodes.length ) {
		return null;
	}

	if ( preferredSlideIndex != null && Number.isFinite( preferredSlideIndex ) ) {
		const match = nodes.find( ( node ) => {
			const idx = slideIndexFromElement( node );
			return (
				idx === preferredSlideIndex &&
				! node.closest?.( '.splide__slide--clone' )
			);
		} );
		if ( match ) {
			return match;
		}
	}

	if ( preferredCardIndex != null && Number.isFinite( preferredCardIndex ) ) {
		const match = nodes.find( ( node ) => {
			const idx = cardIndexFromElement( node );
			return idx === preferredCardIndex;
		} );
		if ( match ) {
			return match;
		}
	}

	const active = nodes.find( ( node ) => isInActiveOrStaticSlide( node ) );
	if ( active ) {
		// Prefer the media frame / hit target over the outer .bm-hero__media cell.
		const nested = nodes.find(
			( node ) =>
				isInActiveOrStaticSlide( node ) &&
				active.contains( node ) &&
				node !== active &&
				( node.classList?.contains( 'bm-media-frame' ) ||
					node.classList?.contains( 'bm-el-hit' ) )
		);
		return nested || active;
	}

	const first =
		nodes.find( ( n ) => ! n.closest?.( '.splide__slide--clone' ) ) ||
		nodes[ 0 ];
	const nestedFirst = nodes.find(
		( node ) =>
			first?.contains?.( node ) &&
			node !== first &&
			( node.classList?.contains( 'bm-media-frame' ) ||
				node.classList?.contains( 'bm-el-hit' ) )
	);
	return nestedFirst || first;
}

/**
 * Theme color for selection chrome.
 *
 * @param {Document} [doc]
 * @return {string}
 */
function selectionThemeColor( doc = document ) {
	try {
		return (
			getComputedStyle( doc.documentElement )
				.getPropertyValue( '--wp-admin-theme-color' )
				.trim() ||
			getComputedStyle( doc.documentElement )
				.getPropertyValue( '--bm-ed-primary' )
				.trim() ||
			'#3525cd'
		);
	} catch ( e ) {
		return '#3525cd';
	}
}

/**
 * Ensure root can host an absolutely positioned floating chrome.
 *
 * @param {HTMLElement} root
 */
function ensureRootPositioning( root ) {
	if ( ! root ) {
		return;
	}
	const doc = root.ownerDocument || document;
	const win = doc.defaultView || window;
	const computed = win.getComputedStyle( root ).position;
	if ( computed === 'static' ) {
		root.style.position = 'relative';
		root.dataset.bmChromeRoot = '1';
	}
}

/**
 * Hosts that can receive a collection-item reorder drop in the preview.
 *
 * @param {ParentNode|null} root
 * @return {Element[]}
 */
export function collectionReorderHosts( root ) {
	if ( ! root || typeof root.querySelectorAll !== 'function' ) {
		return [];
	}
	const preferred = [
		'.bm-tabs__label[data-bm-card-index]',
		'.bm-accordion__item[data-bm-card-index]',
		'.bm-card[data-bm-card-index]',
		'.bm-pricing__card[data-bm-card-index]',
	];
	for ( let i = 0; i < preferred.length; i++ ) {
		const nodes = Array.from( root.querySelectorAll( preferred[ i ] ) );
		if ( nodes.length > 1 ) {
			return nodes;
		}
	}
	return [];
}

/**
 * @param {Element|null} host
 * @return {'tab'|'accordion'|'testimonial'|'plan'|'card'}
 */
function collectionHostKind( host ) {
	if ( ! host ) {
		return 'card';
	}
	if (
		host.classList?.contains( 'bm-tabs__label' ) ||
		host.closest?.( '.bm-tabs' )
	) {
		return 'tab';
	}
	if (
		host.classList?.contains( 'bm-accordion__item' ) ||
		host.closest?.( '.bm-accordion' )
	) {
		return 'accordion';
	}
	if (
		host.classList?.contains( 'bm-testimonial' ) ||
		host.closest?.( '.bm-testimonial' )
	) {
		return 'testimonial';
	}
	if ( host.classList?.contains( 'bm-pricing__card' ) ) {
		return 'plan';
	}
	return 'card';
}

/**
 * @param {'tab'|'accordion'|'testimonial'|'plan'|'card'} kind
 * @return {string}
 */
function collectionMoveLabel( kind ) {
	switch ( kind ) {
		case 'tab':
			return 'Moving tab';
		case 'accordion':
			return 'Moving item';
		case 'testimonial':
			return 'Moving testimonial';
		case 'plan':
			return 'Moving plan';
		default:
			return 'Moving card';
	}
}

/**
 * @param {ParentNode|null} root
 * @param {number}          index
 * @param {Element|null}    fallback
 * @return {Element|null}
 */
function resolveCollectionHost( root, index, fallback = null ) {
	const n = Number( index );
	if ( ! Number.isFinite( n ) ) {
		return fallback;
	}
	const hosts = collectionReorderHosts( root );
	const match = hosts.find( ( host ) => cardIndexFromElement( host ) === n );
	if ( match ) {
		return match;
	}
	if ( root && typeof root.querySelector === 'function' ) {
		return (
			root.querySelector( `[data-bm-card-index="${ n }"]` ) || fallback
		);
	}
	return fallback;
}

/**
 * Build a floating drag image for collection reorder.
 *
 * @param {Document}     doc
 * @param {Element|null} host
 * @param {string}       kind
 * @return {HTMLElement|null}
 */
function createCollectionDragGhost( doc, host, kind ) {
	if ( ! doc?.body ) {
		return null;
	}
	const ghost = doc.createElement( 'div' );
	ghost.className = 'bm-collection-drag-ghost';
	ghost.setAttribute( 'data-bm-collection-ghost', '1' );
	ghost.setAttribute( 'aria-hidden', 'true' );
	const rect = host?.getBoundingClientRect?.();
	const width = rect
		? Math.min( Math.max( Math.round( rect.width ), 140 ), 300 )
		: 200;
	ghost.style.cssText = [
		'position:fixed',
		'top:-1000px',
		'left:-1000px',
		`width:${ width }px`,
		'box-sizing:border-box',
		'padding:10px 12px',
		'border-radius:8px',
		'border:2px solid #3525cd',
		'background:#fff',
		'box-shadow:0 10px 28px rgba(15,23,42,.18)',
		'color:#0f172a',
		'font:600 13px/1.3 system-ui,sans-serif',
		'display:flex',
		'align-items:center',
		'gap:10px',
		'pointer-events:none',
		'z-index:2147483646',
	].join( ';' );

	const badge = doc.createElement( 'span' );
	badge.className = 'bm-collection-drag-ghost__badge';
	badge.style.cssText = [
		'flex:0 0 auto',
		'width:28px',
		'height:28px',
		'border-radius:6px',
		'background:#3525cd',
		'color:#fff',
		'display:inline-flex',
		'align-items:center',
		'justify-content:center',
	].join( ';' );
	const icon = doc.createElement( 'span' );
	icon.className = 'dashicons dashicons-move';
	icon.style.cssText = 'font-size:16px;width:16px;height:16px;line-height:1';
	badge.appendChild( icon );

	const copy = doc.createElement( 'span' );
	copy.className = 'bm-collection-drag-ghost__copy';
	copy.style.cssText =
		'display:flex;flex-direction:column;gap:2px;min-width:0';
	const title = doc.createElement( 'strong' );
	title.textContent = collectionMoveLabel( kind );
	title.style.cssText =
		'font:700 13px/1.2 system-ui,sans-serif;white-space:nowrap';
	const hint = doc.createElement( 'span' );
	hint.textContent = 'Drop on another item to place it';
	hint.style.cssText =
		'font:500 11px/1.2 system-ui,sans-serif;color:#64748b;white-space:nowrap';
	copy.appendChild( title );
	copy.appendChild( hint );

	ghost.appendChild( badge );
	ghost.appendChild( copy );
	doc.body.appendChild( ghost );
	return ghost;
}

/**
 * Clear preview drop-target chrome from a collection reorder drag.
 *
 * @param {ParentNode|null} root
 */
export function clearCollectionReorderDrag( root ) {
	if ( ! root || typeof root.querySelectorAll !== 'function' ) {
		return;
	}
	root.classList?.remove?.( 'is-bm-collection-dragging' );
	root.querySelectorAll( '.is-bm-drop-target' ).forEach( ( node ) => {
		node.classList.remove(
			'is-bm-drop-target',
			'is-bm-drop-before',
			'is-bm-drop-after'
		);
	} );
	root.querySelectorAll( '.is-bm-drag-source' ).forEach( ( node ) => {
		node.classList.remove( 'is-bm-drag-source' );
	} );
	root.querySelectorAll( '[data-bm-collection-drop]' ).forEach( ( node ) => {
		node.removeAttribute( 'data-bm-collection-drop' );
	} );
	const doc = root.ownerDocument || document;
	doc.querySelectorAll?.( '[data-bm-collection-ghost]' ).forEach( ( node ) => {
		node.remove();
	} );
}

/**
 * Floating selection chrome (border + tools) on the preview root.
 *
 * @param {HTMLElement} root Preview / edit root (positioning context)
 * @param {Element|null} element Selected [data-bm-el] node
 * @param {Function} onDeselect
 * @param {object} [options]
 * @param {Function} [options.onRemove] Delete focused collection item (tab/card/…)
 * @param {boolean} [options.canRemove]
 * @param {Function} [options.onDuplicate] Duplicate focused collection item
 * @param {boolean} [options.canDuplicate]
 * @param {Function} [options.onReorder] Move focused item (from, to) => void
 * @param {boolean} [options.canReorder]
 * @param {number|null} [options.reorderFromIndex]
 * @param {string} [options.removeConfirmMessage]
 */
export function syncSelectionChrome( root, element, onDeselect, options = {} ) {
	if ( ! root ) {
		return;
	}
	const doc = root.ownerDocument || document;
	const canRemove =
		Boolean( options.canRemove ) && typeof options.onRemove === 'function';
	const canDuplicate =
		Boolean( options.canDuplicate ) &&
		typeof options.onDuplicate === 'function';
	const canReorder =
		Boolean( options.canReorder ) &&
		typeof options.onReorder === 'function' &&
		Number.isFinite( Number( options.reorderFromIndex ) );
	void options.removeConfirmMessage;

	// Drop legacy in-element chrome (overflow:hidden parents clipped it).
	root.querySelectorAll( '.bm-el-chrome:not(.bm-el-chrome--float)' ).forEach( ( node ) => {
		const parent = node.parentElement;
		node.remove();
		if ( parent?.dataset?.bmPosFix === '1' ) {
			parent.style.position = '';
			delete parent.dataset.bmPosFix;
		}
	} );

	let chrome = root.querySelector( ':scope > .bm-el-chrome--float' );

	if ( ! element || ! root.contains( element ) ) {
		clearCollectionReorderDrag( root );
		chrome?.remove();
		return;
	}

	ensureRootPositioning( root );
	const theme = selectionThemeColor( doc );

	if ( ! chrome ) {
		chrome = doc.createElement( 'div' );
		chrome.className = 'bm-el-chrome bm-el-chrome--float';
		chrome.id = 'bm-el-chrome';
		chrome.setAttribute( 'aria-hidden', 'true' );
		const tools = doc.createElement( 'div' );
		tools.className = 'bm-el-chrome__tools';
		const btn = doc.createElement( 'button' );
		btn.type = 'button';
		btn.className = 'bm-el-deselect';
		btn.setAttribute( 'aria-label', 'Deselect element' );
		btn.textContent = '×';
		tools.appendChild( btn );
		chrome.appendChild( tools );
		root.appendChild( chrome );
	} else if ( ! chrome.id ) {
		chrome.id = 'bm-el-chrome';
	}

	const tools =
		chrome.querySelector( '.bm-el-chrome__tools' ) ||
		( () => {
			const next = doc.createElement( 'div' );
			next.className = 'bm-el-chrome__tools';
			const existing = chrome.querySelector( '.bm-el-deselect' );
			if ( existing ) {
				next.appendChild( existing );
			}
			chrome.appendChild( next );
			return next;
		} )();

	// Remove legacy Advance Edit chrome if present.
	tools.querySelector( '.bm-el-advance-edit' )?.remove();

	// Ensure deselect button exists.
	let deselectBtn = tools.querySelector( '.bm-el-deselect' );
	if ( ! deselectBtn ) {
		deselectBtn = doc.createElement( 'button' );
		deselectBtn.type = 'button';
		deselectBtn.className = 'bm-el-deselect';
		deselectBtn.setAttribute( 'aria-label', 'Deselect element' );
		deselectBtn.textContent = '×';
		tools.appendChild( deselectBtn );
	}

	// Tool order (left → right): drag, duplicate, delete, ×
	let dragBtn = tools.querySelector( '.bm-el-drag' );
	if ( canReorder ) {
		if ( ! dragBtn ) {
			dragBtn = doc.createElement( 'button' );
			dragBtn.type = 'button';
			dragBtn.className = 'bm-el-drag';
			dragBtn.setAttribute( 'aria-label', 'Drag to reorder' );
			dragBtn.title = 'Drag to reorder';
			const icon = doc.createElement( 'span' );
			icon.className = 'dashicons dashicons-move';
			icon.setAttribute( 'aria-hidden', 'true' );
			dragBtn.appendChild( icon );
			tools.insertBefore( dragBtn, deselectBtn );
		}
		dragBtn.draggable = true;
	} else if ( dragBtn ) {
		dragBtn.remove();
		dragBtn = null;
	}

	let duplicateBtn = tools.querySelector( '.bm-el-duplicate' );
	if ( canDuplicate ) {
		if ( ! duplicateBtn ) {
			duplicateBtn = doc.createElement( 'button' );
			duplicateBtn.type = 'button';
			duplicateBtn.className = 'bm-el-duplicate';
			duplicateBtn.setAttribute( 'aria-label', 'Duplicate item' );
			const icon = doc.createElement( 'span' );
			icon.className = 'dashicons dashicons-admin-page';
			icon.setAttribute( 'aria-hidden', 'true' );
			duplicateBtn.appendChild( icon );
			tools.insertBefore( duplicateBtn, deselectBtn );
		}
	} else if ( duplicateBtn ) {
		duplicateBtn.remove();
		duplicateBtn = null;
	}

	let deleteBtn = tools.querySelector( '.bm-el-delete' );
	if ( canRemove ) {
		if ( ! deleteBtn ) {
			deleteBtn = doc.createElement( 'button' );
			deleteBtn.type = 'button';
			deleteBtn.className = 'bm-el-delete';
			deleteBtn.setAttribute( 'aria-label', 'Delete item' );
			const icon = doc.createElement( 'span' );
			icon.className = 'dashicons dashicons-trash';
			icon.setAttribute( 'aria-hidden', 'true' );
			deleteBtn.appendChild( icon );
		}
		// Keep delete immediately left of ×.
		tools.insertBefore( deleteBtn, deselectBtn );
	} else if ( deleteBtn ) {
		deleteBtn.remove();
		deleteBtn = null;
	}

	// Keep duplicate left of delete (or × when delete is absent).
	if ( duplicateBtn ) {
		const before = deleteBtn || deselectBtn;
		tools.insertBefore( duplicateBtn, before );
	}
	// Keep drag left of duplicate (or delete / ×).
	if ( dragBtn ) {
		const before = duplicateBtn || deleteBtn || deselectBtn;
		tools.insertBefore( dragBtn, before );
	}

	deselectBtn.onclick = ( event ) => {
		event.preventDefault();
		event.stopPropagation();
		onDeselect?.();
	};

	if ( duplicateBtn ) {
		duplicateBtn.onclick = ( event ) => {
			event.preventDefault();
			event.stopPropagation();
			options.onDuplicate?.();
		};
	}

	if ( deleteBtn ) {
		deleteBtn.onclick = ( event ) => {
			event.preventDefault();
			event.stopPropagation();
			// Parent shows ConfirmModal — do not use window.confirm here.
			options.onRemove?.();
		};
	}

	if ( dragBtn ) {
		const fromIndex = Number( options.reorderFromIndex );
		dragBtn.onclick = ( event ) => {
			event.preventDefault();
			event.stopPropagation();
		};
		dragBtn.ondragstart = ( event ) => {
			event.stopPropagation();
			if ( ! Number.isFinite( fromIndex ) ) {
				event.preventDefault();
				return;
			}
			// Kill pending title sync/commit without remounting (keeps drag source alive).
			endPreviewContentEditForLinkEditor( { unfreeze: false } );
			try {
				event.dataTransfer.effectAllowed = 'move';
				// Prefer a private MIME so browsers don't treat this as HTML/text paste
				// into contenteditable card fields on drop.
				event.dataTransfer.setData(
					'application/x-bm-collection-index',
					String( fromIndex )
				);
				event.dataTransfer.setData( 'text/plain', String( fromIndex ) );
				event.dataTransfer.setData( 'text/html', '' );
			} catch ( e ) {
				// Some hosts reject custom MIME types.
			}
			clearCollectionReorderDrag( root );
			root.classList.add( 'is-bm-collection-dragging' );
			const sourceHost = resolveCollectionHost( root, fromIndex, element );
			const kind = collectionHostKind( sourceHost || element );
			if ( sourceHost ) {
				sourceHost.classList.add( 'is-bm-drag-source' );
			}
			const ghost = createCollectionDragGhost(
				doc,
				sourceHost || element,
				kind
			);
			if ( ghost && event.dataTransfer?.setDragImage ) {
				try {
					event.dataTransfer.setDragImage( ghost, 20, 18 );
				} catch ( e ) {
					// Ignore hosts that reject custom drag images.
				}
				// Remove after the browser captures the bitmap.
				setTimeout( () => ghost.remove(), 0 );
			} else {
				ghost?.remove();
			}
			collectionReorderHosts( root ).forEach( ( host ) => {
				const idx = cardIndexFromElement( host );
				if ( idx == null || idx === fromIndex ) {
					return;
				}
				host.setAttribute( 'data-bm-collection-drop', '1' );
			} );
		};
		dragBtn.ondragend = () => {
			clearCollectionReorderDrag( root );
		};
	}

	// Root-level drop while a chrome drag is active.
	root.ondragover = ( event ) => {
		if ( ! root.classList.contains( 'is-bm-collection-dragging' ) ) {
			return;
		}
		// Always cancel default so contenteditable / anchors don't eat the drop.
		event.preventDefault();
		event.stopPropagation();
		if ( event.dataTransfer ) {
			event.dataTransfer.dropEffect = 'move';
		}
		const host = event.target?.closest?.(
			'[data-bm-collection-drop="1"]'
		);
		root.querySelectorAll( '.is-bm-drop-target' ).forEach( ( node ) => {
			if ( node !== host ) {
				node.classList.remove( 'is-bm-drop-target' );
			}
		} );
		if ( ! host || ! root.contains( host ) ) {
			return;
		}
		host.classList.add( 'is-bm-drop-target' );
	};
	root.ondrop = ( event ) => {
		if ( ! root.classList.contains( 'is-bm-collection-dragging' ) ) {
			return;
		}
		event.preventDefault();
		event.stopPropagation();
		const host = event.target?.closest?.(
			'[data-bm-collection-drop="1"]'
		);
		const to = host ? cardIndexFromElement( host ) : null;
		let from = Number( options.reorderFromIndex );
		try {
			const raw =
				event.dataTransfer?.getData(
					'application/x-bm-collection-index'
				) || event.dataTransfer?.getData( 'text/plain' );
			if ( raw !== '' && raw != null && /^\d+$/.test( String( raw ).trim() ) ) {
				from = Number( raw );
			}
		} catch ( e ) {
			// Keep reorderFromIndex fallback.
		}
		clearCollectionReorderDrag( root );
		if (
			! Number.isFinite( from ) ||
			! Number.isFinite( to ) ||
			from === to
		) {
			return;
		}
		// Move onto the hovered item's index (stable for grids + vertical lists).
		options.onReorder?.( from, to );
	};

	const rootRect = root.getBoundingClientRect();
	const elRect = element.getBoundingClientRect();
	const top = elRect.top - rootRect.top + root.scrollTop - 2;
	const left = elRect.left - rootRect.left + root.scrollLeft - 2;
	const width = Math.max( 0, elRect.width + 4 );
	const height = Math.max( 0, elRect.height + 4 );

	chrome.style.cssText = [
		'position:absolute',
		`top:${ top }px`,
		`left:${ left }px`,
		`width:${ width }px`,
		`height:${ height }px`,
		'box-sizing:border-box',
		`border:2px solid ${ theme }`,
		'border-radius:4px',
		'pointer-events:none',
		'z-index:9999',
		'margin:0',
		'padding:0',
		'background:transparent',
		'display:block',
	].join( ';' );

	// Always park tools on the outside top of the outline (tabs / cards / images).
	tools.classList.remove( 'is-inside' );

	const toolBtnBase = [
		'box-sizing:border-box',
		'width:22px',
		'height:22px',
		'margin:0',
		'padding:0',
		`background:${ theme }`,
		'color:#fff',
		'cursor:pointer',
		'display:flex',
		'align-items:center',
		'justify-content:center',
		'pointer-events:auto',
		'line-height:1',
		'flex:0 0 auto',
	];

	tools.style.cssText = [
		'position:absolute',
		'top:-2px',
		'right:-2px',
		'transform:translateY(-100%)',
		'display:flex',
		'flex-direction:row',
		'align-items:stretch',
		'pointer-events:auto',
		'z-index:10000',
	].join( ';' );

	const toolButtons = [ dragBtn, duplicateBtn, deleteBtn, deselectBtn ].filter(
		Boolean
	);
	toolButtons.forEach( ( btn, i ) => {
		const isFirst = i === 0;
		const isLast = i === toolButtons.length - 1;
		let radius = '0';
		if ( isFirst && isLast ) {
			radius = '2px 2px 0 0';
		} else if ( isFirst ) {
			radius = '2px 0 0 0';
		} else if ( isLast ) {
			radius = '0 2px 0 0';
		}
		const isDeselect = btn === deselectBtn;
		const isDrag = btn === dragBtn;
		btn.style.cssText = [
			...toolBtnBase,
			`border:2px solid ${ theme }`,
			'border-bottom:none',
			isLast ? '' : 'border-right:none',
			`border-radius:${ radius }`,
			isDeselect ? 'font:700 14px/1 system-ui,sans-serif' : '',
			isDrag ? 'cursor:grab' : '',
		]
			.filter( Boolean )
			.join( ';' );
	} );
}

/**
 * Mark styleable nodes and selected key inside a BM wrapper.
 * Single-element blocks skip hover/active chrome (no border or ×).
 *
 * @param {HTMLElement} root
 * @param {string[]} selectableKeys
 * @param {string|null} selectedElementKey
 * @param {number|null} [preferredSlideIndex]
 * @param {number|null} [preferredCardIndex]
 * @return {Element|null} Selected element node
 */
export function markStyleableAndSelected(
	root,
	selectableKeys,
	selectedElementKey,
	preferredSlideIndex = null,
	preferredCardIndex = null
) {
	if ( ! root ) {
		return null;
	}
	const set = new Set( ( selectableKeys || [] ).filter( Boolean ) );
	const multiSelect = set.size > 1;
	const activeKey = multiSelect ? selectedElementKey : null;
	wrapMediaHitTargets( root, set );

	root.querySelectorAll( '[data-bm-el]' ).forEach( ( node ) => {
		if (
			node.classList?.contains( 'bm-el-chrome' ) ||
			node.classList?.contains( 'bm-el-chrome--float' )
		) {
			return;
		}
		const elKey = node.getAttribute( 'data-bm-el' );
		if ( multiSelect && set.has( elKey ) ) {
			node.setAttribute( 'data-bm-styleable', 'true' );
		} else {
			node.removeAttribute( 'data-bm-styleable' );
		}
		node.removeAttribute( 'data-bm-selected' );
	} );

	// Card shells are clickable selection targets (Features Icon / Blogs / Pricing).
	// Only when the preview has selectable keys (editor), not library-only preview.
	root
		.querySelectorAll(
			'.bm-card[data-bm-card-index], .bm-pricing__card[data-bm-card-index]'
		)
		.forEach( ( node ) => {
			if ( set.size > 0 ) {
				node.setAttribute( 'data-bm-styleable', 'true' );
			} else {
				node.removeAttribute( 'data-bm-styleable' );
			}
			node.removeAttribute( 'data-bm-selected' );
		} );

	const cardShell =
		set.size > 0
			? resolveCardShellHost( root, preferredCardIndex )
			: null;
	if ( cardShell ) {
		if ( activeKey && set.has( activeKey ) ) {
			const nested = resolveStyleableElement(
				root,
				activeKey,
				preferredSlideIndex,
				preferredCardIndex
			);
			if ( nested && nested !== cardShell ) {
				nested.setAttribute( 'data-bm-selected', 'true' );
				// Icons / avatars: ring the clicked host, not the whole card.
				if ( prefersNestedSelectionChrome( activeKey ) ) {
					return nested;
				}
			}
		}
		// Prefer outlining the whole card when a card is focused for
		// content editing (title, body, …).
		cardShell.setAttribute( 'data-bm-selected', 'true' );
		return cardShell;
	}

	if ( ! activeKey || ! set.has( activeKey ) ) {
		return null;
	}

	const found = resolveStyleableElement(
		root,
		activeKey,
		preferredSlideIndex,
		preferredCardIndex
	);
	if ( found ) {
		found.setAttribute( 'data-bm-selected', 'true' );
	}
	return found;
}
