import { useEffect, useMemo, useRef, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import ConfirmModal from '../../components/ConfirmModal';
import { DEVICE_WIDTH_PX } from '../../cssVars';
import {
	dashiconsStylesheetTag,
	fontStylesheetTags,
	frontendRuntimeTags,
	iconStylesheetTags,
	remountPreviewSliders,
	themeAgnosticStylesheetTag,
} from '../previewAssets';
import {
	markStyleableAndSelected,
	slideIndexFromElement,
	cardIndexFromElement,
	isSliderChromeTarget,
	syncSelectionChrome,
	wrapMediaHitTargets,
	forceOpenDetailsForEditing,
	bindForceOpenDetails,
	activateBmTabForEditing,
} from '../../shared/selectionChrome';
import {
	initBmAccordion,
	enforceAccordionExclusiveOpen,
} from '../../shared/bmAccordion';
import {
	bindPreviewContentEdit,
	isPreviewEditChromeTarget,
	PREVIEW_CONTENT_EDIT_CSS,
	subscribePreviewContentEditForceEnd,
} from '../../shared/previewContentEdit';
import { focusBmSliderSlide, releaseBmSliderFocus } from '../../shared/sliderElements';
import { hydratePendingInlineIcons } from '../../shared/iconRegistry';
import {
	appendAccordionItemToAttributes,
	appendCardToAttributes,
	appendTabToAttributes,
	injectPreviewAccordionAddButtons,
	injectPreviewCardsAddButtons,
	injectPreviewTabsAddButtons,
	isAccordionAddButtonTarget,
	isCardsAddButtonTarget,
	isTabsAddButtonTarget,
	PREVIEW_TABS_ADD_CSS,
} from '../../shared/previewTabsAdd';
import PreviewFormatToolbar from '../../components/fields/PreviewFormatToolbar';

export const PREVIEW_VIEWPORTS = [
	{
		id: 'desktop',
		label: __( 'Desktop', 'joc-blockora-morph' ),
		icon: 'desktop',
		maxWidth: null,
	},
	{
		id: 'tablet',
		label: __( 'Tablet', 'joc-blockora-morph' ),
		icon: 'tablet',
		maxWidth: DEVICE_WIDTH_PX.tablet,
	},
	{
		id: 'mobile',
		label: __( 'Mobile', 'joc-blockora-morph' ),
		icon: 'smartphone',
		maxWidth: DEVICE_WIDTH_PX.mobile,
	},
];

/**
 * Device chrome style — tablet/mobile must actually shrink (maxWidth, not w-full).
 *
 * @param {{ id: string, maxWidth: number|null }} viewport
 * @return {object}
 */
export function previewDeviceStyle( viewport ) {
	const max = viewport?.maxWidth;
	if ( ! max ) {
		return { width: '100%', maxWidth: '100%' };
	}
	return { width: '100%', maxWidth: `${ max }px` };
}
const SELECTION_CSS = `
[data-bm-el][data-bm-styleable="true"],
.bm-card[data-bm-card-index][data-bm-styleable="true"],
.bm-pricing__card[data-bm-card-index][data-bm-styleable="true"]{
	cursor:pointer;
	outline:2px solid transparent;
	outline-offset:2px;
	transition:outline-color .12s ease;
}
[data-bm-el][data-bm-styleable="true"]:hover,
.bm-card[data-bm-card-index][data-bm-styleable="true"]:hover,
.bm-pricing__card[data-bm-card-index][data-bm-styleable="true"]:hover{outline-color:rgba(0,124,186,.35)}
[data-bm-el][data-bm-selected="true"],
.bm-card[data-bm-card-index][data-bm-selected="true"],
.bm-pricing__card[data-bm-card-index][data-bm-selected="true"]{
	outline-color:transparent;
}
.bm-el-hit,.bm-media-frame{
	position:relative!important;
}
.bm-el-hit__overlay{
	position:absolute;
	inset:0;
	z-index:2;
	cursor:pointer;
	background:transparent;
	display:block;
}
.bm-hero--video > .bm-el-hit__overlay,
[data-layout="video-background"] > .bm-el-hit__overlay,
.bm-hero__video{
	pointer-events:none!important;
}
#bm-el-chrome{
	position:absolute;
	box-sizing:border-box;
	border:2px solid var(--wp-admin-theme-color,#007cba);
	pointer-events:none;
	z-index:9999;
}
#bm-el-chrome .bm-el-chrome__tools{
	pointer-events:auto;
	position:absolute;
	top:-2px;
	right:-2px;
	transform:translateY(-100%);
	display:flex;
	flex-direction:row;
	align-items:stretch;
	z-index:10000;
}
#bm-el-chrome .bm-el-deselect,
#bm-el-chrome .bm-el-delete,
#bm-el-chrome .bm-el-duplicate,
#bm-el-chrome .bm-el-drag{
	pointer-events:auto;
	box-sizing:border-box;
	width:22px;
	height:22px;
	margin:0;
	padding:0;
	border:2px solid var(--wp-admin-theme-color,#007cba);
	border-bottom:none;
	border-radius:2px 2px 0 0;
	background:var(--wp-admin-theme-color,#007cba);
	color:#fff;
	font:700 14px/1 system-ui,sans-serif;
	cursor:pointer;
	display:flex;
	align-items:center;
	justify-content:center;
	flex:0 0 auto;
	line-height:1;
}
#bm-el-chrome .bm-el-drag{
	cursor:grab;
}
#bm-el-chrome .bm-el-drag:active{
	cursor:grabbing;
}
#bm-el-chrome .bm-el-drag,
#bm-el-chrome .bm-el-duplicate,
#bm-el-chrome .bm-el-delete{
	border-right:none;
	border-radius:2px 0 0 0;
}
#bm-el-chrome .bm-el-drag + .bm-el-duplicate,
#bm-el-chrome .bm-el-drag + .bm-el-delete,
#bm-el-chrome .bm-el-drag + .bm-el-deselect,
#bm-el-chrome .bm-el-duplicate + .bm-el-delete,
#bm-el-chrome .bm-el-duplicate + .bm-el-deselect,
#bm-el-chrome .bm-el-delete + .bm-el-deselect{
	border-radius:0 2px 0 0;
}
#bm-el-chrome .bm-el-drag + .bm-el-duplicate,
#bm-el-chrome .bm-el-drag + .bm-el-delete,
#bm-el-chrome .bm-el-duplicate + .bm-el-delete{
	border-radius:0;
}
#bm-el-chrome .bm-el-drag + .bm-el-duplicate + .bm-el-delete + .bm-el-deselect,
#bm-el-chrome .bm-el-drag + .bm-el-duplicate + .bm-el-deselect,
#bm-el-chrome .bm-el-drag + .bm-el-delete + .bm-el-deselect,
#bm-el-chrome .bm-el-duplicate + .bm-el-delete + .bm-el-deselect{
	border-radius:0 2px 0 0;
}
#bm-el-chrome .bm-el-drag .dashicons,
#bm-el-chrome .bm-el-delete .dashicons,
#bm-el-chrome .bm-el-duplicate .dashicons{
	font-size:14px;
	width:14px;
	height:14px;
	line-height:1;
}
.is-bm-collection-dragging [data-bm-collection-drop="1"]{
	outline:2px dashed rgba(53,37,205,.28);
	outline-offset:3px;
	transition:outline-color .12s ease, background-color .12s ease;
}
.is-bm-collection-dragging .is-bm-drag-source{
	position:relative !important;
	opacity:.42 !important;
	outline:2px dashed rgba(53,37,205,.55) !important;
	outline-offset:3px !important;
	background:rgba(53,37,205,.05) !important;
	filter:saturate(.35);
}
.is-bm-collection-dragging .is-bm-drag-source::after{
	content:"Moving…";
	position:absolute;
	inset:0;
	display:flex;
	align-items:center;
	justify-content:center;
	z-index:5;
	pointer-events:none;
	font:700 12px/1.2 system-ui,sans-serif;
	letter-spacing:.02em;
	color:#3525cd;
	text-shadow:0 1px 0 rgba(255,255,255,.85);
	background:linear-gradient(180deg,rgba(255,255,255,.55),rgba(255,255,255,.28));
}
.is-bm-collection-dragging .is-bm-drop-target{
	outline:2px solid rgba(53,37,205,.9) !important;
	outline-offset:3px;
	background:rgba(53,37,205,.07);
}
${ PREVIEW_CONTENT_EDIT_CSS }
${ PREVIEW_TABS_ADD_CSS }
`;

/** Visual cue only — clicks are blocked in JS so element selection still works. */
const PREVIEW_GUARD_CSS = `
a, button, input[type="submit"], input[type="button"], input[type="reset"], [role="button"] {
	cursor: default !important;
}
`;

/** Classic padding-bottom ratio shell inside .wp-block-embed__wrapper. */
const PREVIEW_EMBED_CSS = `
.bm-media-frame{display:block!important;width:var(--bm-mf-img-width, var(--bm-image-width, 100%))!important;max-width:100%!important;box-sizing:border-box!important;line-height:0!important}
.bm-testimonial__meta>.bm-el-hit,.bm-testimonial__meta>.bm-media-frame{width:fit-content!important;flex-shrink:0!important;max-width:100%!important;order:var(--bm-author-image-order, 0)}
.bm-testimonial__meta>.bm-testimonial__avatar{order:var(--bm-author-image-order, 0)}
.bm-testimonial__meta .bm-testimonial__avatar{width:var(--bm-avatar-size,2.75rem)!important;height:var(--bm-avatar-size,2.75rem)!important;max-width:none!important}
.bm-hero__embed,.bm-hero__embed.wp-block-embed{display:block!important;width:100%!important;max-width:100%!important;height:auto!important;min-height:0!important;aspect-ratio:auto!important;overflow:hidden!important;line-height:0!important;box-sizing:border-box!important;object-fit:unset!important}
.bm-hero__embed .wp-block-embed__wrapper{position:relative!important;display:block!important;width:100%!important;height:auto!important;aspect-ratio:auto!important;overflow:hidden!important;line-height:0!important;box-sizing:border-box!important}
.bm-hero__embed .bm-embed-ratio,.bm-hero__embed .wp-block-embed__wrapper>div[style*="padding-bottom"]{display:block!important;max-width:100%!important;box-sizing:border-box!important}
.bm-hero__embed .bm-embed-ratio iframe,.bm-hero__embed .wp-block-embed__wrapper>div[style*="padding-bottom"] iframe,.bm-hero__embed iframe{position:absolute!important;top:0!important;left:0!important;width:100%!important;max-width:none!important;height:100%!important;min-height:0!important;aspect-ratio:auto!important;border:0!important;margin:0!important;padding:0!important;object-fit:unset!important}
`;

/** Fallback when the device stage has not laid out yet. */
const PREVIEW_VH_FALLBACK_PX = 640;

/**
 * Map vh/dvh/svh/lvh to px using a stable preview viewport height.
 * Auto-sized iframes must not keep 100vh — it tracks iframe height and grows forever.
 *
 * @param {string} css
 * @param {number} vhPx
 * @return {string}
 */
export function rewriteViewportHeightUnits( css, vhPx ) {
	const px = Math.max(
		1,
		Math.round( Number( vhPx ) || PREVIEW_VH_FALLBACK_PX )
	);
	return String( css || '' ).replace(
		/(-?\d*\.?\d+)\s*(vh|dvh|svh|lvh)\b/gi,
		( _, num ) => {
			const n = parseFloat( num );
			if ( ! Number.isFinite( n ) ) {
				return `${ px }px`;
			}
			return `${ Math.round( ( n / 100 ) * px ) }px`;
		}
	);
}

const INTERACTIVE_SEL =
	'a[href], button, input[type="submit"], input[type="button"], input[type="reset"], [role="button"]';

/**
 * Stop links/buttons from navigating or submitting inside preview iframes.
 *
 * @param {Event} event
 */
function blockPreviewNavigation( event ) {
	const target = event.target;
	if ( ! target?.closest ) {
		return;
	}
	if ( target.closest( '.bm-el-deselect, .bm-el-delete, .bm-el-duplicate, .bm-el-drag, .bm-el-chrome__tools' ) ) {
		return;
	}
	const interactive = target.closest( INTERACTIVE_SEL );
	if ( ! interactive ) {
		return;
	}
	event.preventDefault();
}

/**
 * @param {KeyboardEvent} event
 */
function blockPreviewKeyActivation( event ) {
	if ( event.key !== 'Enter' && event.key !== ' ' ) {
		return;
	}
	// Space/Enter must type (or commit) inside preview contenteditables —
	// button labels live on <a href>, which would otherwise eat the key.
	const target = event.target;
	if (
		target?.isContentEditable ||
		target?.closest?.( '[contenteditable="true"]' )
	) {
		return;
	}
	blockPreviewNavigation( event );
}

/**
 * Viewport switcher + iframe preview so @media rules match the device width.
 */
export default function PreviewFrame( {
	html,
	srcDoc,
	children,
	className = '',
	frameClassName = '',
	defaultViewport = 'desktop',
	viewport: viewportProp,
	onViewportChange,
	showLabel = true,
	minHeight = 360,
	selectableKeys = null,
	selectedElementKey = null,
	focusSlideIndex = null,
	focusCardIndex = null,
	focusButtonIndex = null,
	onElementSelect,
	stageNotice = null,
	contentEditEnabled = false,
	fieldSchema = null,
	attributes = null,
	setAttributes = null,
	markEphemeral = null,
	onContentEditSessionStart = null,
	onContentEditSessionCommit = null,
	onContentEditSessionEnd = null,
	onDocumentUndo = null,
	onDocumentRedo = null,
	/** Optional overlay rendered at the bottom-right of the preview canvas. */
	stageActions = null,
	/** Delete focused collection item (tab / card / accordion row). */
	onRemoveItem = null,
	canRemoveItem = false,
	removeItemConfirmTitle = '',
	removeItemConfirmLabel = '',
	removeItemConfirmDescription = '',
	/** Duplicate focused collection item (card / tab / accordion row). */
	onDuplicateItem = null,
	canDuplicateItem = false,
	onReorderItem = null,
	canReorderItem = false,
} ) {
	const [ internalViewport, setInternalViewport ] = useState( defaultViewport );
	const isControlled = viewportProp !== undefined;
	const viewport = isControlled ? viewportProp : internalViewport;
	const setViewport = ( next ) => {
		if ( ! isControlled ) {
			setInternalViewport( next );
		}
		onViewportChange?.( next );
	};

	const active =
		PREVIEW_VIEWPORTS.find( ( v ) => v.id === viewport ) || PREVIEW_VIEWPORTS[ 0 ];

	const iframeRef = useRef( null );
	const deviceRef = useRef( null );
	const onElementSelectRef = useRef( onElementSelect );
	onElementSelectRef.current = onElementSelect;
	const selectedKeyRef = useRef( selectedElementKey );
	selectedKeyRef.current = selectedElementKey;
	const focusSlideIndexRef = useRef( focusSlideIndex );
	focusSlideIndexRef.current = focusSlideIndex;
	const focusCardIndexRef = useRef( focusCardIndex );
	focusCardIndexRef.current = focusCardIndex;
	const focusButtonIndexRef = useRef( focusButtonIndex );
	focusButtonIndexRef.current = focusButtonIndex;
	const attributesRef = useRef( attributes );
	attributesRef.current = attributes;
	const setAttributesRef = useRef( setAttributes );
	setAttributesRef.current = setAttributes;
	const markEphemeralRef = useRef( markEphemeral );
	markEphemeralRef.current = markEphemeral;
	const onContentEditSessionStartRef = useRef( onContentEditSessionStart );
	onContentEditSessionStartRef.current = onContentEditSessionStart;
	const onContentEditSessionCommitRef = useRef( onContentEditSessionCommit );
	onContentEditSessionCommitRef.current = onContentEditSessionCommit;
	const onContentEditSessionEndRef = useRef( onContentEditSessionEnd );
	onContentEditSessionEndRef.current = onContentEditSessionEnd;
	const onDocumentUndoRef = useRef( onDocumentUndo );
	onDocumentUndoRef.current = onDocumentUndo;
	const onDocumentRedoRef = useRef( onDocumentRedo );
	onDocumentRedoRef.current = onDocumentRedo;
	const fieldSchemaRef = useRef( fieldSchema );
	fieldSchemaRef.current = fieldSchema;
	const onRemoveItemRef = useRef( onRemoveItem );
	onRemoveItemRef.current = onRemoveItem;
	const canRemoveItemRef = useRef( canRemoveItem );
	canRemoveItemRef.current = canRemoveItem;
	const onDuplicateItemRef = useRef( onDuplicateItem );
	onDuplicateItemRef.current = onDuplicateItem;
	const canDuplicateItemRef = useRef( canDuplicateItem );
	canDuplicateItemRef.current = canDuplicateItem;
	const onReorderItemRef = useRef( onReorderItem );
	onReorderItemRef.current = onReorderItem;
	const canReorderItemRef = useRef( canReorderItem );
	canReorderItemRef.current = canReorderItem;
	const [ chromeDeleteOpen, setChromeDeleteOpen ] = useState( false );
	const requestChromeDeleteRef = useRef( () => {} );
	requestChromeDeleteRef.current = () => {
		if ( ! canRemoveItemRef.current || ! onRemoveItemRef.current ) {
			return;
		}
		setChromeDeleteOpen( true );
	};
	const contentEditSelectionRef = useRef( '' );
	const [ formatSession, setFormatSession ] = useState( null );
	/** Freeze epoch — bump only when unfreezing so live markup can apply. */
	const [ freezeEpoch, setFreezeEpoch ] = useState( 0 );
	/** True while preview contenteditable is actively focused / formatting. */
	const editSurfaceActiveRef = useRef( false );
	/** Hold iframe markup steady while actively editing so attr sync does not remount. */
	const frozenDocHtmlRef = useRef( '' );
	const docHtmlRef = useRef( '' );

	const bodyHtml = html?.__html || '';
	const styleCss = html?.styleCss || '';
	const styleCssRef = useRef( styleCss );
	styleCssRef.current = styleCss;

	const resolvePreviewVhPx = () => {
		// Use the stage (fixed flex viewport), not the device/iframe — those grow
		// with content and would reintroduce the 100vh feedback loop.
		const stageH = deviceRef.current?.parentElement?.clientHeight || 0;
		return Math.max( stageH, minHeight, PREVIEW_VH_FALLBACK_PX );
	};

	const applyPreviewStyles = ( doc ) => {
		if ( ! doc?.head ) {
			return;
		}
		let el = doc.getElementById( 'bm-preview-vars' );
		if ( ! el ) {
			el = doc.createElement( 'style' );
			el.id = 'bm-preview-vars';
			doc.head.appendChild( el );
		}
		el.textContent = rewriteViewportHeightUnits(
			styleCssRef.current || '',
			resolvePreviewVhPx()
		);
	};

	const measureFrame = ( frame ) => {
		try {
			const doc = frame?.contentDocument;
			if ( ! doc?.body ) {
				return;
			}
			// Ensure padding-bottom ratio shells contribute height before measuring.
			doc.querySelectorAll(
				'.bm-hero__embed .bm-embed-ratio, .bm-hero__embed .wp-block-embed__wrapper > div[style*="padding-bottom"]'
			).forEach( ( wrap ) => {
				if ( !( wrap instanceof HTMLElement ) ) {
					return;
				}
				if ( wrap.clientHeight > 0 ) {
					return;
				}
				const w = wrap.clientWidth || wrap.parentElement?.clientWidth || 0;
				if ( w > 0 ) {
					wrap.style.minHeight = `${ Math.round( ( w * 9 ) / 16 ) }px`;
				}
			} );
			const h = Math.max(
				doc.body.scrollHeight,
				doc.documentElement.scrollHeight,
				doc.body.offsetHeight,
				minHeight
			);
			// Hard cap so a stray vh unit cannot runaway-grow the iframe.
			const cap = Math.max( resolvePreviewVhPx() * 3, minHeight );
			frame.style.height = `${ Math.min( h, cap ) }px`;
		} catch ( e ) {
			// ignore cross-origin
		}
	};

	const bindEmbedResize = ( frame ) => {
		try {
			const doc = frame?.contentDocument;
			if ( ! doc ) {
				return () => {};
			}
			const onEmbedLoad = () => {
				window.requestAnimationFrame( () => measureFrame( frame ) );
			};
			const embeds = doc.querySelectorAll(
				'.bm-hero__embed iframe, .bm-hero__iframe, .bm-media-frame iframe'
			);
			embeds.forEach( ( el ) => {
				el.addEventListener( 'load', onEmbedLoad );
			} );
			// Aspect-ratio boxes settle after layout; remeasure a few times.
			const t1 = window.setTimeout( onEmbedLoad, 80 );
			const t2 = window.setTimeout( onEmbedLoad, 320 );
			const t3 = window.setTimeout( onEmbedLoad, 800 );
			return () => {
				embeds.forEach( ( el ) => {
					el.removeEventListener( 'load', onEmbedLoad );
				} );
				window.clearTimeout( t1 );
				window.clearTimeout( t2 );
				window.clearTimeout( t3 );
			};
		} catch ( e ) {
			return () => {};
		}
	};

	const selectableSet = useMemo(
		() => new Set( ( selectableKeys || [] ).filter( Boolean ) ),
		[ selectableKeys ]
	);
	const selectableSetRef = useRef( selectableSet );
	selectableSetRef.current = selectableSet;
	const selectionEnabled = selectableSet.size > 0;

	// Rebuild iframe document only when markup changes — styleCss updates in place.
	const docHtml = useMemo( () => {
		if ( srcDoc ) {
			return srcDoc;
		}
		if ( ! bodyHtml ) {
			return '';
		}
		const runtimeTags = String( bodyHtml ).includes( 'data-bm-slider' )
			? frontendRuntimeTags()
			: '';
		const iconTags = iconStylesheetTags( bodyHtml );
		return `<!DOCTYPE html><html data-bm-editor-preview="1"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="referrer" content="strict-origin-when-cross-origin">${ dashiconsStylesheetTag() }${ fontStylesheetTags() }${ themeAgnosticStylesheetTag() }${ iconTags }${ runtimeTags }<style>html,body{margin:0;padding:0;background:#fff;}body{padding:22px;box-sizing:border-box;position:relative;}.dashicons{display:inline-block;line-height:1;text-decoration:inherit;vertical-align:top;}</style><style id="bm-preview-guard">${ PREVIEW_GUARD_CSS }</style><style id="bm-preview-embed">${ PREVIEW_EMBED_CSS }</style><style id="bm-preview-selection">${ SELECTION_CSS }</style><style id="bm-preview-vars"></style></head><body data-bm-editor-preview="1">${ bodyHtml }</body></html>`;
	}, [ srcDoc, bodyHtml ] );
	docHtmlRef.current = docHtml;
	// freezeEpoch: ensure we re-read the ref after unfreeze.
	void freezeEpoch;

	// Freeze via ref so attr sync re-renders do not remount the iframe mid-edit.
	const iframeDocHtml = editSurfaceActiveRef.current
		? frozenDocHtmlRef.current || docHtml
		: docHtml;

	// Parent-window clicks (inspector Style reference, etc.) do not blur the
	// iframe contenteditable — release the freeze so markup can remount.
	// Do NOT treat the portaled format toolbar as an "outside" click.
	useEffect( () => {
		const releaseFreeze = () => {
			if ( ! editSurfaceActiveRef.current ) {
				return;
			}
			editSurfaceActiveRef.current = false;
			frozenDocHtmlRef.current = '';
			setFreezeEpoch( ( n ) => n + 1 );
		};
		const unsubscribe = subscribePreviewContentEditForceEnd( releaseFreeze );
		const onPointerDownCapture = ( event ) => {
			if ( ! editSurfaceActiveRef.current ) {
				return;
			}
			if ( isPreviewEditChromeTarget( event.target ) ) {
				return;
			}
			const frame = iframeRef.current;
			if (
				frame &&
				event.target instanceof Node &&
				( event.target === frame || frame.contains( event.target ) )
			) {
				return;
			}
			try {
				frame?.contentDocument?.activeElement?.blur?.();
			} catch ( e ) {
				// ignore cross-origin / detached
			}
			if ( ! editSurfaceActiveRef.current ) {
				return;
			}
			editSurfaceActiveRef.current = false;
			setFreezeEpoch( ( n ) => n + 1 );
		};
		window.addEventListener( 'pointerdown', onPointerDownCapture, true );
		return () => {
			unsubscribe();
			window.removeEventListener(
				'pointerdown',
				onPointerDownCapture,
				true
			);
		};
	}, [] );

	// Hide iframe until load so raw/empty document never flashes.
	// Also remount handlers depend on iframeDocHtml only — do not reset ready on viewport
	// resize (that would flash blank when the window narrows).
	const [ frameReady, setFrameReady ] = useState( false );

	useEffect( () => {
		setFrameReady( false );
	}, [ iframeDocHtml ] );

	useEffect( () => {
		const frame = iframeRef.current;
		if ( ! frame || ! iframeDocHtml ) {
			return;
		}

		const measure = () => measureFrame( frame );

		const imgCleanups = [];

		const onLoad = () => {
			setFrameReady( true );
			try {
				applyPreviewStyles( frame.contentDocument );
			} catch ( e ) {
				// ignore
			}
			measure();
			try {
				const doc = frame.contentDocument;
				if ( ! doc ) {
					return;
				}
				// Remeasure when images finish so tall heroes aren't clipped.
				doc.querySelectorAll( 'img' ).forEach( ( img ) => {
					if ( img.complete ) {
						return;
					}
					img.addEventListener( 'load', measure );
					img.addEventListener( 'error', measure );
					imgCleanups.push( () => {
						img.removeEventListener( 'load', measure );
						img.removeEventListener( 'error', measure );
					} );
				} );
				imgCleanups.push( bindEmbedResize( frame ) );
				hydratePendingInlineIcons( doc ).then( () => {
					measure();
				} );
				// Init / remount Splide after markup + runtime script load.
				if ( doc.querySelector( '[data-bm-slider]' ) ) {
					remountPreviewSliders( doc );
					const holdIdx = focusSlideIndexRef.current;
					if ( holdIdx != null || selectedKeyRef.current ) {
						window.setTimeout( () => {
							focusBmSliderSlide(
								holdIdx != null ? holdIdx : 0,
								doc
							);
						}, 0 );
						window.setTimeout( () => {
							focusBmSliderSlide(
								holdIdx != null ? holdIdx : 0,
								doc
							);
						}, 140 );
					}
					window.setTimeout( measure, 120 );
					window.setTimeout( measure, 400 );
				}
			} catch ( e ) {
				// ignore
			}
		};

		frame.addEventListener( 'load', onLoad );
		try {
			if ( frame.contentDocument?.readyState === 'complete' ) {
				onLoad();
			}
		} catch ( e ) {
			// ignore
		}
		const t = window.setTimeout( onLoad, 50 );
		const t2 = window.setTimeout( measure, 300 );
		return () => {
			frame.removeEventListener( 'load', onLoad );
			window.clearTimeout( t );
			window.clearTimeout( t2 );
			imgCleanups.forEach( ( fn ) => fn() );
		};
	}, [ iframeDocHtml, minHeight ] );

	// Never navigate / submit from preview links & buttons (frontend stays live).
	useEffect( () => {
		const frame = iframeRef.current;
		if ( ! frame || ! iframeDocHtml ) {
			return undefined;
		}

		let doc = null;
		const bind = () => {
			try {
				doc = frame.contentDocument;
				if ( ! doc ) {
					return;
				}
				doc.addEventListener( 'click', blockPreviewNavigation, true );
				doc.addEventListener( 'auxclick', blockPreviewNavigation, true );
				doc.addEventListener( 'keydown', blockPreviewKeyActivation, true );
			} catch ( e ) {
				// ignore
			}
		};

		frame.addEventListener( 'load', bind );
		bind();

		return () => {
			frame.removeEventListener( 'load', bind );
			try {
				doc?.removeEventListener( 'click', blockPreviewNavigation, true );
				doc?.removeEventListener( 'auxclick', blockPreviewNavigation, true );
				doc?.removeEventListener( 'keydown', blockPreviewKeyActivation, true );
			} catch ( e ) {
				// ignore
			}
		};
	}, [ iframeDocHtml ] );

	// Push CSS vars into the iframe without remounting (keeps element selection).
	// Rewrite vh → px against the device stage so 100vh cannot chase iframe height.
	useEffect( () => {
		const frame = iframeRef.current;
		if ( ! frame || ! iframeDocHtml ) {
			return undefined;
		}
		const applyStyles = () => {
			try {
				applyPreviewStyles( frame.contentDocument );
				measureFrame( frame );
			} catch ( e ) {
				// ignore
			}
		};
		frame.addEventListener( 'load', applyStyles );
		applyStyles();

		const device = deviceRef.current;
		const stage = device?.parentElement;
		let ro = null;
		if ( stage && typeof ResizeObserver !== 'undefined' ) {
			ro = new ResizeObserver( applyStyles );
			ro.observe( stage );
		}

		return () => {
			frame.removeEventListener( 'load', applyStyles );
			ro?.disconnect();
		};
	}, [ iframeDocHtml, styleCss, viewport, minHeight ] );

	// Click-to-select styleable elements inside the iframe.
	useEffect( () => {
		const frame = iframeRef.current;
		if ( ! frame || ! iframeDocHtml || ! selectionEnabled ) {
			return undefined;
		}

		let doc = null;
		const onClick = ( event ) => {
			if (
				event.target?.closest?.(
					'.bm-el-deselect, .bm-el-delete, .bm-el-duplicate, .bm-el-drag, .bm-el-chrome__tools'
				)
			) {
				return;
			}
			if ( isTabsAddButtonTarget( event.target ) ) {
				event.preventDefault();
				event.stopPropagation();
				if ( typeof setAttributesRef.current !== 'function' ) {
					return;
				}
				const added = appendTabToAttributes(
					attributesRef.current,
					fieldSchemaRef.current
				);
				if ( ! added ) {
					return;
				}
				setAttributesRef.current( added.patch );
				onElementSelectRef.current?.( 'tabTitle', {
					slideIndex: null,
					cardIndex: added.newIndex,
					buttonIndex: null,
				} );
				return;
			}
			if ( isAccordionAddButtonTarget( event.target ) ) {
				event.preventDefault();
				event.stopPropagation();
				if ( typeof setAttributesRef.current !== 'function' ) {
					return;
				}
				const added = appendAccordionItemToAttributes(
					attributesRef.current,
					fieldSchemaRef.current
				);
				if ( ! added ) {
					return;
				}
				setAttributesRef.current( added.patch );
				onElementSelectRef.current?.( 'accordionTitle', {
					slideIndex: null,
					cardIndex: added.newIndex,
					buttonIndex: null,
				} );
				return;
			}
			if ( isCardsAddButtonTarget( event.target ) ) {
				event.preventDefault();
				event.stopPropagation();
				if ( typeof setAttributesRef.current !== 'function' ) {
					return;
				}
				const added = appendCardToAttributes(
					attributesRef.current,
					fieldSchemaRef.current
				);
				if ( ! added ) {
					return;
				}
				setAttributesRef.current( added.patch );
				onElementSelectRef.current?.( 'title', {
					slideIndex: null,
					cardIndex: added.newIndex,
					buttonIndex: null,
				} );
				return;
			}
			if ( isSliderChromeTarget( event.target ) ) {
				return;
			}
			const target = event.target?.closest?.( '[data-bm-el]' );
			const key =
				target &&
				doc?.body?.contains( target ) &&
				selectableSet.has( target.getAttribute( 'data-bm-el' ) )
					? target.getAttribute( 'data-bm-el' )
					: null;

			const slideHit =
				event.target?.closest?.(
					'[data-bm-slide-index], .splide__slide, .bm-hero--slide'
				) || null;
			const slideIndex = slideHit
				? slideIndexFromElement( slideHit ) ??
				  slideIndexFromElement( target || event.target )
				: slideIndexFromElement( target || event.target );

			const cardHit =
				event.target?.closest?.( '[data-bm-card-index]' ) || null;
			const cardIndex = cardHit
				? cardIndexFromElement( cardHit ) ??
				  cardIndexFromElement( target || event.target )
				: cardIndexFromElement( target || event.target );

			const btnHit =
				event.target?.closest?.( '[data-bm-btn-index]' ) || null;
			let buttonIndex = null;
			if ( btnHit ) {
				const raw = btnHit.getAttribute( 'data-bm-btn-index' );
				const parsed = raw != null ? Number( raw ) : NaN;
				if ( Number.isFinite( parsed ) && parsed >= 0 ) {
					buttonIndex = parsed;
				}
			}

			if ( key || slideIndex != null || cardIndex != null ) {
				event.preventDefault();
				event.stopPropagation();
				if ( slideIndex != null && doc ) {
					focusBmSliderSlide( slideIndex, doc );
				}
				activateBmTabForEditing(
					target || event.target,
					cardIndex
				);
				onElementSelectRef.current?.( key, {
					slideIndex,
					cardIndex,
					buttonIndex:
						key === 'buttons' || buttonIndex != null
							? buttonIndex
							: null,
				} );
				return;
			}

			onElementSelectRef.current?.( null, {
				slideIndex: null,
				cardIndex: null,
			} );
		};

		const onPointerDown = ( event ) => {
			if ( isSliderChromeTarget( event.target ) ) {
				return;
			}
			const target = event.target?.closest?.(
				'[data-bm-el], [data-bm-slide-index], .splide__slide, .bm-hero--slide'
			);
			if ( target && doc?.body?.contains( target ) ) {
				// Splide Drag listens to mousedown/touchstart; stop those too.
				event.stopPropagation();
			}
		};

		const bind = () => {
			try {
				doc = frame.contentDocument;
				if ( ! doc ) {
					return;
				}
				forceOpenDetailsForEditing( doc, {
					focusCardIndex: focusCardIndexRef.current,
				} );
				initBmAccordion( doc );
				enforceAccordionExclusiveOpen( doc.body );
				injectPreviewTabsAddButtons( doc.body, {
					enabled: typeof setAttributesRef.current === 'function',
					fieldSchema: fieldSchemaRef.current,
				} );
				injectPreviewAccordionAddButtons( doc.body, {
					enabled: typeof setAttributesRef.current === 'function',
					fieldSchema: fieldSchemaRef.current,
				} );
				injectPreviewCardsAddButtons( doc.body, {
					enabled: typeof setAttributesRef.current === 'function',
					fieldSchema: fieldSchemaRef.current,
				} );
				if ( focusCardIndexRef.current != null ) {
					activateBmTabForEditing(
						doc.body,
						focusCardIndexRef.current
					);
				}
				doc.addEventListener( 'click', onClick, true );
				doc.addEventListener( 'pointerdown', onPointerDown, true );
				doc.addEventListener( 'mousedown', onPointerDown, true );
				doc.addEventListener( 'touchstart', onPointerDown, true );
			} catch ( e ) {
				// ignore
			}
		};

		frame.addEventListener( 'load', bind );
		bind();

		let unbindToggle;
		try {
			unbindToggle = bindForceOpenDetails( frame.contentDocument );
		} catch ( e ) {
			unbindToggle = undefined;
		}

		return () => {
			frame.removeEventListener( 'load', bind );
			unbindToggle?.();
			try {
				doc?.removeEventListener( 'click', onClick, true );
				doc?.removeEventListener( 'pointerdown', onPointerDown, true );
				doc?.removeEventListener( 'mousedown', onPointerDown, true );
				doc?.removeEventListener( 'touchstart', onPointerDown, true );
			} catch ( e ) {
				// ignore
			}
		};
	}, [ iframeDocHtml, selectionEnabled, selectableSet ] );

	// Sync selected outline + deselect chrome; remeasure on viewport resize/transition.
	useEffect( () => {
		const frame = iframeRef.current;
		const device = deviceRef.current;
		if ( ! frame || ! iframeDocHtml ) {
			return undefined;
		}

		let rafId = 0;
		let transitionTimer = 0;

		const sync = () => {
			try {
				const doc = frame.contentDocument;
				if ( ! doc?.body ) {
					return;
				}
				wrapMediaHitTargets( doc.body, selectableSetRef.current );
				forceOpenDetailsForEditing( doc, {
					focusCardIndex: focusCardIndexRef.current,
				} );
				initBmAccordion( doc );
				enforceAccordionExclusiveOpen( doc.body );
				injectPreviewTabsAddButtons( doc.body, {
					enabled: typeof setAttributesRef.current === 'function',
					fieldSchema: fieldSchemaRef.current,
				} );
				injectPreviewAccordionAddButtons( doc.body, {
					enabled: typeof setAttributesRef.current === 'function',
					fieldSchema: fieldSchemaRef.current,
				} );
				injectPreviewCardsAddButtons( doc.body, {
					enabled: typeof setAttributesRef.current === 'function',
					fieldSchema: fieldSchemaRef.current,
				} );
				if ( focusCardIndexRef.current != null ) {
					activateBmTabForEditing(
						doc.body,
						focusCardIndexRef.current
					);
				}
				// Library / read-only previews have no selectable keys — never
				// paint card outlines or × chrome (Number(null)===0 used to
				// falsely select the first card).
				if ( selectableSetRef.current.size > 0 ) {
					const found = markStyleableAndSelected(
						doc.body,
						Array.from( selectableSetRef.current ),
						selectedKeyRef.current,
						focusSlideIndexRef.current,
						focusCardIndexRef.current
					);
					syncSelectionChrome(
						doc.body,
						found,
						() => {
							if ( selectedKeyRef.current ) {
								onElementSelectRef.current?.( null, {
									deselectElement: true,
								} );
								return;
							}
							onElementSelectRef.current?.( null, {
								slideIndex: null,
								cardIndex: null,
							} );
						},
						{
							canRemove: Boolean( canRemoveItemRef.current ),
							onRemove: () => requestChromeDeleteRef.current?.(),
							canDuplicate: Boolean( canDuplicateItemRef.current ),
							onDuplicate: () => onDuplicateItemRef.current?.(),
							canReorder: Boolean( canReorderItemRef.current ),
							onReorder: ( from, to ) =>
								onReorderItemRef.current?.( from, to ),
							reorderFromIndex: focusCardIndexRef.current,
						}
					);
				} else {
					syncSelectionChrome( doc.body, null, () => {} );
				}

				// Keep iframe height in sync after layout changes.
				measureFrame( frame );
			} catch ( e ) {
				// ignore
			}
		};

		const syncSoon = () => {
			if ( rafId ) {
				window.cancelAnimationFrame( rafId );
			}
			rafId = window.requestAnimationFrame( () => {
				rafId = 0;
				sync();
			} );
		};

		// Track the device width transition (300ms) so chrome stays aligned.
		const trackTransition = () => {
			const started = performance.now();
			const tick = ( now ) => {
				sync();
				if ( now - started < 360 ) {
					rafId = window.requestAnimationFrame( tick );
				} else {
					rafId = 0;
					sync();
				}
			};
			if ( rafId ) {
				window.cancelAnimationFrame( rafId );
			}
			rafId = window.requestAnimationFrame( tick );
			window.clearTimeout( transitionTimer );
			transitionTimer = window.setTimeout( sync, 380 );
		};

		frame.addEventListener( 'load', sync );
		sync();
		trackTransition();

		const observers = [];
		try {
			const doc = frame.contentDocument;
			if ( doc?.body && typeof ResizeObserver !== 'undefined' ) {
				const ro = new ResizeObserver( syncSoon );
				ro.observe( doc.body );
				ro.observe( doc.documentElement );
				observers.push( ro );
			}
		} catch ( e ) {
			// ignore
		}

		if ( device && typeof ResizeObserver !== 'undefined' ) {
			const deviceRo = new ResizeObserver( syncSoon );
			deviceRo.observe( device );
			observers.push( deviceRo );
		}

		window.addEventListener( 'resize', syncSoon );

		return () => {
			frame.removeEventListener( 'load', sync );
			window.removeEventListener( 'resize', syncSoon );
			window.clearTimeout( transitionTimer );
			if ( rafId ) {
				window.cancelAnimationFrame( rafId );
			}
			observers.forEach( ( ro ) => ro.disconnect() );
		};
		// styleCss updates via #bm-preview-vars — do not rebuild chrome (flicker).
	}, [ iframeDocHtml, viewport, selectedElementKey, selectableSet, focusSlideIndex, focusCardIndex, minHeight, contentEditEnabled, fieldSchema, canRemoveItem, canDuplicateItem, canReorderItem ] );

	// Inline content editing (text / button labels) — additive to click-to-select.
	useEffect( () => {
		if ( ! contentEditEnabled || typeof setAttributes !== 'function' ) {
			return undefined;
		}
		const frame = iframeRef.current;
		if ( ! frame || ! iframeDocHtml || ! selectedElementKey ) {
			return undefined;
		}

		const selectionId = [
			selectedElementKey,
			focusSlideIndex,
			focusCardIndex,
			focusButtonIndex,
		].join( ':' );
		const autoFocus = contentEditSelectionRef.current !== selectionId;
		contentEditSelectionRef.current = selectionId;

		let cleanup = () => {};
		const bind = () => {
			try {
				const doc = frame.contentDocument;
				if ( ! doc?.body ) {
					return;
				}
				cleanup();
				cleanup = bindPreviewContentEdit( doc.body, {
					fieldSchema: fieldSchemaRef.current || [],
					getAttributes: () => attributesRef.current || {},
					setAttributes: ( patch ) => {
						setAttributesRef.current?.( patch );
					},
					markEphemeral: () => {
						markEphemeralRef.current?.();
					},
					onSessionCommit: ( before, after ) => {
						onContentEditSessionCommitRef.current?.( before, after );
					},
					onDocumentUndo: () =>
						Boolean( onDocumentUndoRef.current?.() ),
					onDocumentRedo: () =>
						Boolean( onDocumentRedoRef.current?.() ),
					selectedElementKey,
					focusSlideIndex,
					focusCardIndex,
					focusButtonIndex,
					autoFocus,
					onSessionStart: ( session ) => {
						onContentEditSessionStartRef.current?.();
						setFormatSession( session );
					},
					onEditSurfaceActive: ( active ) => {
						const next = Boolean( active );
						if ( next ) {
							if ( ! editSurfaceActiveRef.current ) {
								frozenDocHtmlRef.current =
									docHtmlRef.current || '';
							}
							editSurfaceActiveRef.current = true;
							return;
						}
						if ( ! editSurfaceActiveRef.current ) {
							return;
						}
						editSurfaceActiveRef.current = false;
						// Re-render so the iframe picks up live markup.
						setFreezeEpoch( ( n ) => n + 1 );
					},
					onSessionEnd: () => {
						onContentEditSessionEndRef.current?.();
						setFormatSession( null );
						if ( editSurfaceActiveRef.current ) {
							editSurfaceActiveRef.current = false;
							setFreezeEpoch( ( n ) => n + 1 );
						}
					},
				} );
			} catch ( e ) {
				// ignore
			}
		};

		frame.addEventListener( 'load', bind );
		bind();
		return () => {
			frame.removeEventListener( 'load', bind );
			cleanup();
		};
	}, [
		iframeDocHtml,
		contentEditEnabled,
		setAttributes,
		selectedElementKey,
		focusSlideIndex,
		focusCardIndex,
		focusButtonIndex,
	] );

	// Close pending chrome delete if selection / remove availability changes.
	useEffect( () => {
		setChromeDeleteOpen( false );
	}, [ selectedElementKey, focusCardIndex, canRemoveItem ] );
	useEffect( () => {
		const frame = iframeRef.current;
		if ( ! frame || ! iframeDocHtml ) {
			return undefined;
		}
		const applyHold = () => {
			try {
				const doc = frame.contentDocument;
				if ( ! doc?.body ) {
					return;
				}
				if ( focusSlideIndex != null || selectedElementKey ) {
					focusBmSliderSlide(
						focusSlideIndex != null ? focusSlideIndex : 0,
						doc
					);
				} else {
					releaseBmSliderFocus( doc );
				}
			} catch ( e ) {
				// ignore
			}
		};
		frame.addEventListener( 'load', applyHold );
		applyHold();
		const t = window.setTimeout( applyHold, 100 );
		return () => {
			frame.removeEventListener( 'load', applyHold );
			window.clearTimeout( t );
		};
	}, [ iframeDocHtml, selectedElementKey, focusSlideIndex ] );

	return (
		<div className={ `bm-preview-frame flex min-h-0 min-w-0 flex-1 flex-col ${ className }` }>
			<div className="bm-preview-frame__toolbar flex flex-wrap items-center gap-3 border-b border-outline-variant/20 bg-surface-lowest px-4 py-2.5">
				{ showLabel && (
					<span className="text-caps-micro font-bold uppercase tracking-widest text-outline">
						{ __( 'Preview', 'joc-blockora-morph' ) }
					</span>
				) }
				<div
					className="bm-preview-frame__viewports flex items-center gap-1"
					role="tablist"
					aria-label={ __( 'Preview viewport', 'joc-blockora-morph' ) }
				>
					{ PREVIEW_VIEWPORTS.map( ( item ) => {
						const isActive = item.id === viewport;
						return (
							<button
								key={ item.id }
								type="button"
								role="tab"
								aria-selected={ isActive }
								className={
									isActive
										? 'bm-viewport-btn is-active'
										: 'bm-viewport-btn'
								}
								onClick={ () => setViewport( item.id ) }
							>
								<span
									className={ `dashicons dashicons-${ item.icon }` }
									aria-hidden="true"
								/>
								{ item.label }
							</button>
						);
					} ) }
				</div>
				<span className="ml-auto hidden text-[11px] text-outline sm:inline">
					{ active.id === 'desktop'
						? __( 'Full width', 'joc-blockora-morph' )
						: `${ active.maxWidth }px` }
				</span>
			</div>

			{ stageNotice }

			<div className="bm-preview-frame__stage relative flex min-h-0 flex-1 justify-center overflow-auto bg-surface-low p-4 sm:p-6">
				<div
					ref={ deviceRef }
					className={ `bm-preview-frame__device custom-scrollbar max-h-full overflow-x-hidden overflow-y-auto rounded-xl border border-outline-variant/30 bg-white shadow-sm transition-[max-width] duration-300 ease-[cubic-bezier(0.2,0,0,1)] ${ frameClassName }` }
					data-viewport={ active.id }
					style={ previewDeviceStyle( active ) }
				>
					{ iframeDocHtml ? (
						<iframe
							ref={ iframeRef }
							title={ __( 'Block preview', 'joc-blockora-morph' ) }
							className="bm-preview-frame__iframe block w-full border-0"
							style={ {
								minHeight: `${ minHeight }px`,
								height: `${ minHeight }px`,
								opacity: frameReady ? 1 : 0,
								transition: 'opacity 0.12s ease',
							} }
							// First-party srcDoc preview: needs scripts (slider) and
							// contentDocument access (selection / live CSS). Do not set
							// sandbox with both allow-scripts + allow-same-origin — browsers
							// treat that combo as escapable and warn.
							srcDoc={ iframeDocHtml }
						/>
					) : (
						<div className="bm-preview-frame__content p-4 sm:p-6">{ children }</div>
					) }
				</div>
				{ stageActions ? (
					<div className="bm-preview-frame__stage-actions">
						{ stageActions }
					</div>
				) : null }
			</div>

			{ formatSession ? (
				<PreviewFormatToolbar session={ formatSession } />
			) : null }

			<ConfirmModal
				open={ chromeDeleteOpen }
				title={
					removeItemConfirmTitle ||
					__( 'Delete this item?', 'joc-blockora-morph' )
				}
				description={
					removeItemConfirmDescription ||
					__(
						'This item and its content will be permanently removed from this block.',
						'joc-blockora-morph'
					)
				}
				icon="trash"
				tone="danger"
				confirmLabel={
					removeItemConfirmLabel ||
					__( 'Delete', 'joc-blockora-morph' )
				}
				onCancel={ () => setChromeDeleteOpen( false ) }
				onConfirm={ () => {
					setChromeDeleteOpen( false );
					onRemoveItemRef.current?.();
					onElementSelectRef.current?.( null, {
						deselectElement: true,
					} );
				} }
			/>
		</div>
	);
}
