/**
 * Shared Fields / Customize inspector for Gutenberg + frontend editor + admin.
 * One control surface — do not duplicate field/style UI elsewhere.
 */
import { Button, SelectControl, TabPanel } from '@wordpress/components';
import {
	createElement,
	Fragment,
	useEffect,
	useMemo,
	useRef,
	useState,
} from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import StyleFieldList from '../components/StyleFieldList';
import SchemaFieldControl from '../components/fields/SchemaFieldControl';
import ElementsOrderField from '../components/fields/ElementsOrderField';
import GutenbergWysiwygField from '../components/fields/GutenbergWysiwygField';
import {
	companionTagField,
	isFoldedCompanionTag,
} from '../components/fields/TagField';
import ElementToolbar from '../components/ElementToolbar';
import SlidesList, {
	remapFocusSlideIndex,
} from '../components/SlidesList';
import {
	collectionItemChromeActionsAllowed,
	collectionItemsLabel,
	collectionMinItems,
	collectionSharesIcon,
	findCollectionRepeater,
	isPricingCollectionBlock,
	partialTouchesSharedIcon,
	pickSharedIconPartial,
} from './collectionRepeater';
import { isElementVisible, visibilityKeysForField } from '../interpolate';
import {
	effective,
	isMediaFullscreen,
	merge,
	normalize,
	setBreakpointVar,
	setSharedDesktopVar,
	styleableFieldsInOrder,
} from '../cssVars';
import { isStyleFieldVisibleAtBreakpoint } from '../styleFieldDefaults';
import { useButtonLibrary } from './buttonLibrary';
import { scrollInspectorToTop } from './inspectorScroll';
import StyleScopeGate, { myBlockEditorUrl } from './StyleScopeGate';
import {
	BLOCK_STYLE_SCOPE_KEY,
	rememberStyleScope,
} from './styleScopeMemory';
import {
	elementsOrderDefaultsFromSchema,
	serializeElementsOrder,
	setElementsOrderAtBreakpoint,
} from './elementsOrder';
import {
	effectiveElementsVisibility,
	serializeElementsVisibility,
	setVisibilityMapAtBreakpoint,
} from './elementsVisibility';
import {
	buildIndependentInstancePatch,
	INSTANCE_SCOPE_FORK_VERSION,
	isIndependentInstanceScope,
	needsIndependentInstanceFork,
} from './instanceStyleFork';
import {
	focusBmSliderSlide,
	slideLayoutLabel,
	slideOrderDefaults,
	SLIDE_LAYOUT_OPTIONS,
} from './sliderElements';
import {
	createDefaultHeroSlide,
	MIN_HERO_SLIDES,
} from './sliderHelpers';
import SlideElementsList from './SlideElementsList';
import {
	effectiveSlideLayoutValues,
	ensureSlideLayoutCssVars,
	isColumnsSlideLayout,
	isSlideLevelStyleGroup,
	slideStyleGroupsFor,
	columnMediaVisibilityKeyBreakpoints,
	mediaAllowsFullscreenFrame,
} from './slideLayoutStyle';
/**
 * Label for the active viewport (visibility / order badges).
 *
 * @param {string} breakpoint
 * @return {string}
 */
function breakpointLabel( breakpoint ) {
	if ( breakpoint === 'tablet' ) {
		return __( 'Tablet', 'joc-blockora-morph' );
	}
	if ( breakpoint === 'mobile' ) {
		return __( 'Mobile', 'joc-blockora-morph' );
	}
	return __( 'Desktop', 'joc-blockora-morph' );
}

/**
 * Design-tab element picker button (dims when Content visibility is Hidden).
 *
 * @param {object}   field
 * @param {object}   options
 * @param {Function} options.onClick
 * @param {object}   options.attributes
 * @param {string}   [options.breakpoint]
 * @param {string}   [options.key]
 * @param {number}   [options.index] 1-based list number
 * @return {import('react').ReactElement}
 */
function ElementPickerButton(
	field,
	{ onClick, attributes, breakpoint = 'desktop', key, index, selected = false }
) {
	const visible = isElementVisible( field?.key, attributes, breakpoint );
	const children = [];
	if ( index != null && Number( index ) > 0 ) {
		children.push(
			createElement(
				'span',
				{
					key: 'index',
					className: 'bm-gutenberg-element-btn__index',
					'aria-hidden': true,
				},
				String( index )
			)
		);
	}
	children.push(
		createElement(
			'span',
			{ key: 'label', className: 'bm-gutenberg-element-btn__label' },
			field.label || field.key
		)
	);
	if ( ! visible ) {
		children.push(
			createElement(
				'span',
				{ key: 'badge', className: 'bm-gutenberg-element-btn__badge' },
				sprintf(
					/* translators: %s: Desktop, Tablet, or Mobile */
					__( 'Hidden · %s', 'joc-blockora-morph' ),
					breakpointLabel( breakpoint )
				)
			)
		);
	}
	children.push(
		createElement(
			'span',
			{ key: 'action', className: 'bm-gutenberg-element-btn__action' },
			__( 'Edit', 'joc-blockora-morph' ),
			createElement( 'span', {
				className: 'dashicons dashicons-arrow-right-alt2',
				'aria-hidden': true,
			} )
		)
	);
	return createElement(
		'button',
		{
			key: key || field.key,
			type: 'button',
			'aria-current': selected ? 'true' : undefined,
			className:
				'bm-gutenberg-element-btn' +
				( visible ? '' : ' is-element-hidden' ) +
				( selected ? ' is-selected' : '' ),
			onClick,
		},
		...children
	);
}

/**
 * Bucket content fields into readable sidebar groups (order preserved).
 *
 * @param {Array<object>} fields
 * @return {Array<{ group: string, fields: Array<object> }>}
 */
export function groupContentFields( fields ) {
	const labels = {
		slides: __( 'Slides', 'joc-blockora-morph' ),
		collection: __( 'Items', 'joc-blockora-morph' ),
		media: __( 'Media', 'joc-blockora-morph' ),
		content: __( 'Content', 'joc-blockora-morph' ),
		actions: __( 'Buttons, links & logos', 'joc-blockora-morph' ),
	};
	const kindFor = ( field ) => {
		const type = String( field?.type || 'text' );
		const itemType = String( field?.itemType || '' );
		if (
			itemType === 'slide' ||
			field?.key === 'slides' ||
			( type === 'repeater' && itemType === 'slide' )
		) {
			return 'slides';
		}
		if (
			type === 'repeater' &&
			itemType &&
			itemType !== 'button' &&
			itemType !== 'logo'
		) {
			return 'collection';
		}
		if ( type === 'media' || type === 'image' ) {
			return 'media';
		}
		if ( type === 'repeater' || type === 'link' ) {
			return 'actions';
		}
		return 'content';
	};
	const buckets = new Map();
	const order = [];
	( fields || [] ).forEach( ( field ) => {
		if ( ! field?.key ) {
			return;
		}
		const kind = kindFor( field );
		if ( ! buckets.has( kind ) ) {
			buckets.set( kind, [] );
			order.push( kind );
		}
		buckets.get( kind ).push( field );
	} );
	return order.map( ( kind ) => {
		const groupFields = buckets.get( kind );
		let group = labels[ kind ] || kind;
		if ( kind === 'collection' && groupFields?.[ 0 ]?.label ) {
			group = String( groupFields[ 0 ].label );
		}
		return {
			group,
			fields: groupFields,
		};
	} );
}

/**
 * Layout media fields (e.g. background video) — not in the Elements stack.
 *
 * @param {Array<object>} fieldSchema
 * @param {string[]}      [orderDefaults]
 * @return {Array<object>}
 */
export function layoutMediaFields( fieldSchema = [], orderDefaults = [] ) {
	const order = Array.isArray( orderDefaults ) ? orderDefaults : [];
	return ( fieldSchema || [] ).filter(
		( field ) =>
			field?.key &&
			( field.type === 'media' || field.type === 'image' ) &&
			field.elementStyle !== true &&
			field.ui !== 'styles' &&
			! field.styleOnly &&
			! order.includes( field.key )
	);
}

/**
 * Resolve visibility wiring shared by Fields + Customize content editors.
 * Reads/writes the map for the active viewport (desktop / tablet / mobile).
 *
 * @param {object}   meta
 * @param {object}   attributes
 * @param {Function} setAttributes
 * @param {string}   [breakpoint]
 * @return {{ visibilityKeys: string[], visibility: object, visibilityBreakpoint: string, onVisibilityChange: Function }}
 */
export function contentFieldVisibility(
	meta,
	attributes,
	setAttributes,
	breakpoint = 'desktop',
	options = {}
) {
	let orderField = options.orderField;
	if ( ! orderField ) {
		orderField = ( meta.fieldSchema || [] ).find(
			( field ) => field?.type === 'elementsOrder'
		);
		// Hero slider: elements-order lives under slides.fields.
		if ( ! orderField ) {
			const slidesField = ( meta.fieldSchema || [] ).find(
				( field ) =>
					field?.key === 'slides' && Array.isArray( field.fields )
			);
			orderField = ( slidesField?.fields || [] ).find(
				( field ) => field?.type === 'elementsOrder'
			);
		}
	}
	const defaultAttrs =
		options.defaultAttrs != null
			? options.defaultAttrs
			: meta.defaultAttrs || {};
	const visibilityKeys = visibilityKeysForField(
		orderField || {},
		defaultAttrs
	);
	const bp = [ 'desktop', 'tablet', 'mobile' ].includes( breakpoint )
		? breakpoint
		: 'desktop';
	return {
		visibilityKeys,
		visibilityBreakpoint: bp,
		visibility: effectiveElementsVisibility(
			attributes?.elementsVisibility,
			bp,
			visibilityKeys
		),
		onVisibilityChange: ( nextMap ) =>
			setAttributes( {
				elementsVisibility: serializeElementsVisibility(
					setVisibilityMapAtBreakpoint(
						attributes?.elementsVisibility,
						bp,
						nextMap && typeof nextMap === 'object' ? nextMap : {},
						visibilityKeys
					),
					visibilityKeys
				),
			} ),
	};
}

/**
 * One content field via SchemaFieldControl — same path as the Fields tab.
 */
export function ContentFieldEditor( {
	field,
	attributes,
	setAttributes,
	visibilityKeys = [],
	visibility = {},
	visibilityBreakpoint = 'desktop',
	onVisibilityChange,
	visibilityKeyBreakpoints = null,
	onEditButtonStyles,
	renderRichText,
	fieldSchema = [],
	allowManageButtonItems = true,
	onFocusItem,
	previewScope = null,
} ) {
	if ( ! field?.key || field.type === 'elementsOrder' ) {
		return null;
	}

	const richText =
		typeof renderRichText === 'function'
			? renderRichText
			: ( { field: f, value, onChange } ) =>
					createElement( GutenbergWysiwygField, {
						field: f,
						value,
						onChange,
					} );

	const tagField = companionTagField( field.key, fieldSchema );
	const options = {
		hideElementsOrder: true,
		hideContentEditor: true,
		onEditButtonStyles,
		allowManageButtonItems,
		visibilityKeys,
		onVisibilityChange,
		visibility,
		visibilityBreakpoint,
		visibilityKeyBreakpoints,
		onFocusItem,
		previewScope,
	};
	if ( tagField ) {
		options.tagField = tagField;
		options.tagValue = attributes[ tagField.key ];
		options.onTagChange = ( next ) =>
			setAttributes( { [ tagField.key ]: next } );
	}

	return createElement( SchemaFieldControl, {
		field,
		value: attributes[ field.key ],
		onChange: ( next ) => setAttributes( { [ field.key ]: next } ),
		options,
		renderRichText: richText,
	} );
}

/**
 * Content field controls (visibility toggles). Element reorder lives on the Order tab.
 */
export function FieldControls( {
	meta,
	attributes,
	setAttributes,
	onEditButtonStyles,
	renderRichText,
	breakpoint = 'desktop',
	allowManageButtonItems = false,
	enableStyleScopeGate = false,
	onFocusCardIndex,
} ) {
	const schema = meta.fieldSchema || [];
	const fields = schema.filter(
		( field ) =>
			field?.type !== 'elementsOrder' &&
			! field?.styleOnly &&
			field?.ui !== 'styles' &&
			! isFoldedCompanionTag( field, schema )
	);
	const {
		visibilityKeys,
		visibility,
		visibilityBreakpoint,
		onVisibilityChange,
	} = contentFieldVisibility( meta, attributes, setAttributes, breakpoint );

	const visibilityKeyBreakpoints = useMemo( () => {
		const fromVars = String(
			effective( attributes?.cssVars, 'desktop' )[
				'--bm-columns-breakpoint'
			] ?? ''
		).trim();
		const columns =
			Boolean( fromVars ) ||
			( meta.styleSchema || [] ).some( ( group ) =>
				( group?.fields || [] ).some(
					( field ) =>
						field?.key === '--bm-columns-breakpoint' ||
						field?.type === 'columnsBreakpoint'
				)
			);
		return columnMediaVisibilityKeyBreakpoints( columns );
	}, [ attributes?.cssVars, meta.styleSchema ] );

	const canEditButtonStyles =
		typeof onEditButtonStyles === 'function' &&
		( ! enableStyleScopeGate ||
			isIndependentInstanceScope( attributes?.styleScopeChoices ) );

	if ( ! fields.length ) {
		return createElement(
			'p',
			null,
			__( 'No content fields for this block.', 'joc-blockora-morph' )
		);
	}

	const groups = groupContentFields( fields );

	return createElement(
		'div',
		{ className: 'bm-inspector-stack' },
		groups.map( ( group ) =>
			createElement(
				'div',
				{
					key: group.group,
					className: 'bm-inspector-card',
				},
				createElement(
					'strong',
					{ className: 'bm-inspector-card__title' },
					group.group
				),
				...group.fields.map( ( field ) =>
					createElement( ContentFieldEditor, {
						key: field.key,
						field,
						attributes,
						setAttributes,
						visibilityKeys,
						visibility,
						visibilityBreakpoint,
						onVisibilityChange,
						visibilityKeyBreakpoints,
						onEditButtonStyles: canEditButtonStyles
							? onEditButtonStyles
							: undefined,
						renderRichText,
						fieldSchema: schema,
						allowManageButtonItems,
						onFocusItem:
							typeof onFocusCardIndex === 'function'
								? onFocusCardIndex
								: undefined,
					} )
				)
			)
		)
	);
}

/**
 * Style controls + element picker. Breakpoint is controlled when
 * `breakpoint` + `onBreakpointChange` are passed (Gutenberg device sync);
 * otherwise local state (frontend editor).
 */
export function StyleControls( {
	meta,
	attributes,
	setAttributes,
	selectedElementKey,
	onSelectElement,
	onClearElement,
	focusButtonIndex = null,
	breakpoint: breakpointProp,
	onBreakpointChange,
	onViewSwitch,
	buttonLibrary,
	breakpointHints = 'editor',
	onEditButtonStyles,
	renderRichText,
	elementsOpen = false,
	onElementsOpenChange,
	enableStyleScopeGate = false,
	libraryCssVars: libraryCssVarsProp,
	onEnsureSidebar,
	styleScopeSurfaceId = '',
	focusSlideIndex: focusSlideIndexProp,
	onFocusSlideIndexChange,
	focusCardIndex: focusCardIndexProp,
	onFocusCardIndexChange,
	allowElementReorder = false,
} ) {
	const [ localBreakpoint, setLocalBreakpoint ] = useState( 'desktop' );
	const [ localElementsOpen, setLocalElementsOpen ] = useState( false );
	const [ localFocusSlideIndex, setLocalFocusSlideIndex ] = useState( null );
	const [ localFocusCardIndex, setLocalFocusCardIndex ] = useState( null );
	const focusSlideControlled =
		typeof onFocusSlideIndexChange === 'function';
	const focusSlideIndex = focusSlideControlled
		? focusSlideIndexProp
		: localFocusSlideIndex;
	const setFocusSlideIndex = focusSlideControlled
		? onFocusSlideIndexChange
		: setLocalFocusSlideIndex;
	const focusCardControlled =
		typeof onFocusCardIndexChange === 'function';
	const focusCardIndex = focusCardControlled
		? focusCardIndexProp
		: localFocusCardIndex;
	const setFocusCardIndex = focusCardControlled
		? onFocusCardIndexChange
		: setLocalFocusCardIndex;
	const [ addSlideLayout, setAddSlideLayout ] = useState( 'simple' );
	const [ designSubTab, setDesignSubTab ] = useState( 'items' ); // slides | items | general

	// Canvas / preview click on a slide → always show Slides drill-in.
	const prevFocusSlideRef = useRef( focusSlideIndex );
	useEffect( () => {
		if (
			focusSlideIndex != null &&
			focusSlideIndex !== prevFocusSlideRef.current
		) {
			setDesignSubTab( 'slides' );
		}
		prevFocusSlideRef.current = focusSlideIndex;
	}, [ focusSlideIndex ] );

	// Canvas / preview click on a card (or clearing card focus) → Items tab
	// showing shared element styles (not a per-item drill-in).
	const prevFocusCardRef = useRef( focusCardIndex );
	useEffect( () => {
		if (
			focusCardIndex != null &&
			focusCardIndex !== prevFocusCardRef.current
		) {
			setDesignSubTab( 'items' );
		} else if (
			focusCardIndex == null &&
			prevFocusCardRef.current != null
		) {
			setDesignSubTab( 'items' );
		}
		prevFocusCardRef.current = focusCardIndex;
	}, [ focusCardIndex ] );
	const [ localLibraryCssVars, setLocalLibraryCssVars ] = useState( () =>
		normalize( libraryCssVarsProp || meta.cssVars || {} )
	);
	const [ optimisticScope, setOptimisticScope ] = useState( null );
	const surfaceId =
		styleScopeSurfaceId ||
		( meta?.id ? `ub-${ meta.id }` : 'anon' );
	// One choice for the whole block — not per element.
	const styleScopeKey = BLOCK_STYLE_SCOPE_KEY;
	const storedChoices =
		attributes?.styleScopeChoices &&
		typeof attributes.styleScopeChoices === 'object' &&
		! Array.isArray( attributes.styleScopeChoices )
			? attributes.styleScopeChoices
			: {};
	// Only "instance" (Customize this instance) persists. Linked = no choice.
	const persistedChoice =
		storedChoices[ styleScopeKey ] === 'instance' ? 'instance' : null;
	// Only attributes (or in-flight optimistic unlock) count — session memory
	// must not reopen editing on a still-linked placement.
	const independentInstance =
		persistedChoice === 'instance' ||
		isIndependentInstanceScope( storedChoices ) ||
		( optimisticScope?.key === styleScopeKey &&
			optimisticScope.choice === 'instance' );

	useEffect( () => {
		if ( ! independentInstance ) {
			return;
		}
		rememberStyleScope( surfaceId, styleScopeKey, 'instance' );
	}, [ surfaceId, styleScopeKey, independentInstance ] );

	// Promote legacy per-element "instance" scopes to a full content+style fork.
	// Linked placements with leftover "library" keys are left alone — writing
	// empty styleScopeChoices would dirty the post for no user action.
	useEffect( () => {
		if ( ! enableStyleScopeGate ) {
			return;
		}
		const keys = Object.keys( storedChoices );
		if ( ! keys.length ) {
			return;
		}
		const blockChoice = storedChoices[ BLOCK_STYLE_SCOPE_KEY ];
		const hadInstance =
			blockChoice === 'instance' ||
			keys.some( ( key ) => storedChoices[ key ] === 'instance' );
		if ( ! hadInstance ) {
			return;
		}
		if (
			isIndependentInstanceScope( storedChoices ) &&
			Number( storedChoices.__forkVersion ) === INSTANCE_SCOPE_FORK_VERSION
		) {
			const hasLegacyKeys = keys.some(
				( key ) =>
					key !== BLOCK_STYLE_SCOPE_KEY && key !== '__forkVersion'
			);
			if ( ! hasLegacyKeys ) {
				return;
			}
			setAttributes( {
				styleScopeChoices: {
					[ BLOCK_STYLE_SCOPE_KEY ]: 'instance',
					__forkVersion: INSTANCE_SCOPE_FORK_VERSION,
				},
			} );
			return;
		}
		setAttributes(
			buildIndependentInstancePatch(
				attributes,
				libraryCssVarsProp != null
					? normalize( libraryCssVarsProp )
					: localLibraryCssVars,
				meta
			)
		);
		// eslint-disable-next-line react-hooks/exhaustive-deps -- migrate once on mount
	}, [ enableStyleScopeGate ] );

	const setStyleScopeChoice = ( choice ) => {
		if ( choice !== 'instance' ) {
			return;
		}
		rememberStyleScope( surfaceId, styleScopeKey, 'instance' );
		setOptimisticScope( { key: styleScopeKey, choice: 'instance' } );
		// Snapshot library + overrides so block styles are independent.
		// Button Source / library wiring stays as authored.
		setAttributes(
			buildIndependentInstancePatch(
				attributes,
				libraryCssVarsProp != null
					? normalize( libraryCssVarsProp )
					: localLibraryCssVars,
				meta
			)
		);
	};
	const elementsExpanded =
		typeof onElementsOpenChange === 'function'
			? elementsOpen
			: localElementsOpen;
	const setElementsExpanded =
		typeof onElementsOpenChange === 'function'
			? onElementsOpenChange
			: setLocalElementsOpen;
	const controlled =
		typeof onBreakpointChange === 'function' &&
		typeof breakpointProp === 'string';
	const breakpoint = controlled ? breakpointProp : localBreakpoint;
	const setBreakpoint = controlled
		? onBreakpointChange
		: setLocalBreakpoint;

	const libraryCssVars =
		libraryCssVarsProp != null
			? normalize( libraryCssVarsProp )
			: localLibraryCssVars;

	useEffect( () => {
		if ( libraryCssVarsProp != null ) {
			return;
		}
		setLocalLibraryCssVars( normalize( meta.cssVars || {} ) );
	}, [ meta.id, meta.cssVars, libraryCssVarsProp ] );

	const cssVars = useMemo(
		() => merge( libraryCssVars || {}, attributes.cssVars || {} ),
		[ libraryCssVars, attributes.cssVars ]
	);
	const activeVars = useMemo(
		() =>
			effective(
				independentInstance
					? normalize( attributes.cssVars || {} )
					: cssVars,
				breakpoint
			),
		[
			independentInstance,
			libraryCssVars,
			attributes.cssVars,
			cssVars,
			breakpoint,
		]
	);

	// Migrate older "instance" placements (sparse overrides) to a full fork once.
	useEffect( () => {
		if (
			! enableStyleScopeGate ||
			! independentInstance ||
			! needsIndependentInstanceFork( storedChoices )
		) {
			return;
		}
		setAttributes(
			buildIndependentInstancePatch( attributes, libraryCssVars, meta )
		);
		// eslint-disable-next-line react-hooks/exhaustive-deps -- one-shot migration by fork version
	}, [ enableStyleScopeGate, independentInstance, storedChoices ] );

	const styleable = useMemo(
		() =>
			styleableFieldsInOrder(
				meta.fieldSchema || [],
				attributes?.elementsOrder,
				{
					defaultAttrs: meta.defaultAttrs || {},
					markup: meta.markup || '',
					breakpoint,
				}
			),
		[
			meta.fieldSchema,
			meta.defaultAttrs,
			meta.markup,
			attributes?.elementsOrder,
			breakpoint,
		]
	);
	const elementsOrderDefaults = useMemo(
		() =>
			elementsOrderDefaultsFromSchema(
				meta.fieldSchema || [],
				meta.defaultAttrs || {}
			),
		[ meta.fieldSchema, meta.defaultAttrs ]
	);
	const sliderSettingsFields = useMemo(
		() =>
			( meta.fieldSchema || [] ).filter(
				( field ) =>
					field?.ui === 'styles' &&
					! field?.styleOnly &&
					field?.type !== 'elementsOrder'
			),
		[ meta.fieldSchema ]
	);
	const slides = useMemo(
		() =>
			Array.isArray( attributes?.slides ) ? attributes.slides : [],
		[ attributes?.slides ]
	);
	const isSliderBlock = useMemo(
		() =>
			( meta.fieldSchema || [] ).some(
				( field ) =>
					field?.key === 'slides' && field?.itemType === 'slide'
			),
		[ meta.fieldSchema ]
	);
	const collectionField = useMemo(
		() =>
			isSliderBlock
				? null
				: findCollectionRepeater( meta.fieldSchema || [] ),
		[ meta.fieldSchema, isSliderBlock ]
	);
	const collectionKey = collectionField?.key || null;
	const collectionItems = useMemo( () => {
		if ( ! collectionKey ) {
			return [];
		}
		const raw = attributes?.[ collectionKey ];
		return Array.isArray( raw ) ? raw : [];
	}, [ attributes, collectionKey ] );
	const itemsTabLabel = useMemo(
		() => collectionItemsLabel( meta, collectionField || {} ),
		[ meta, collectionField ]
	);
	const hasSlideElements = isSliderBlock && styleable.length > 0;
	/** Items | General tabs for any collection repeater (cards, FAQ, tabs, …). */
	const hasCollectionElements = Boolean( collectionField );
	// Pricing only: plans repeater (My Blocks keep a custom slug).
	const allowCollectionElementReorder =
		allowElementReorder &&
		( collectionField?.key === 'plans' ||
			collectionField?.itemType === 'plan' ||
			isPricingCollectionBlock( meta ) );
	const groups = useMemo( () => {
		const raw = meta.styleSchema || [];
		if ( ! hasSlideElements && ! hasCollectionElements ) {
			return raw;
		}
		// Layout + spacing moved to Design → Slides (per-slide).
		return raw.filter(
			( group ) => ! isSlideLevelStyleGroup( group?.group )
		);
	}, [ meta.styleSchema, hasSlideElements, hasCollectionElements ] );

	useEffect( () => {
		if ( hasSlideElements ) {
			setDesignSubTab( 'slides' );
			return;
		}
		if ( hasCollectionElements ) {
			setDesignSubTab( 'items' );
		}
	}, [ hasSlideElements, hasCollectionElements, collectionKey ] );
	const selectedField = useMemo(
		() => styleable.find( ( f ) => f.key === selectedElementKey ) || null,
		[ styleable, selectedElementKey ]
	);
	const selectElement = onSelectElement || ( () => {} );
	const selectSlideElement = ( slideIndex, fieldKey ) => {
		setDesignSubTab( 'slides' );
		setFocusSlideIndex( slideIndex );
		focusBmSliderSlide( slideIndex );
		selectElement( fieldKey );
	};
	const selectCollectionElement = ( fieldKey ) => {
		setDesignSubTab( 'items' );
		// Element styles are shared; don't pin a repeater row unless the canvas did.
		selectElement( fieldKey );
	};
	const clearElement = onClearElement || ( () => {
		selectElement( null );
	} );
	const leaveSlide = () => {
		setFocusSlideIndex( null );
		selectElement( null );
	};
	const openSlide = ( slideIndex ) => {
		setDesignSubTab( 'slides' );
		setFocusSlideIndex( slideIndex );
		focusBmSliderSlide( slideIndex );
		selectElement( null );
		setElementsExpanded( true );
	};
	const isSingleStyleable =
		styleable.length === 1 &&
		! hasSlideElements &&
		! hasCollectionElements;

	const activeSlide =
		focusSlideIndex != null && slides[ focusSlideIndex ]
			? slides[ focusSlideIndex ]
			: null;
	const activeCard =
		focusCardIndex != null && collectionItems[ focusCardIndex ]
			? collectionItems[ focusCardIndex ]
			: null;
	const toolbarButtons = activeSlide
		? activeSlide.buttons
		: activeCard
			? activeCard.buttons
			: attributes.buttons;
	const onToolbarButtonsChange = ( next ) => {
		const patch =
			activeSlide && focusSlideIndex != null
				? {
						slides: slides.map( ( slide, i ) =>
							i === focusSlideIndex
								? { ...slide, buttons: next }
								: slide
						),
				  }
				: activeCard && focusCardIndex != null && collectionKey
					? {
							[ collectionKey ]: collectionItems.map(
								( card, i ) =>
									i === focusCardIndex
										? { ...card, buttons: next }
										: card
							),
					  }
					: { buttons: next };
		setAttributes( patch );
	};

	const canRemoveSlide = slides.length > MIN_HERO_SLIDES;
	const addSlide = () => {
		const next = [
			...slides,
			createDefaultHeroSlide( { layout: addSlideLayout } ),
		];
		setAttributes( { slides: next } );
		setDesignSubTab( 'slides' );
		openSlide( next.length - 1 );
	};
	const removeSlide = ( slideIndex ) => {
		if ( ! canRemoveSlide ) {
			return;
		}
		const next = slides.filter( ( _, i ) => i !== slideIndex );
		setAttributes( { slides: next } );
		if ( focusSlideIndex == null ) {
			return;
		}
		if ( focusSlideIndex === slideIndex ) {
			leaveSlide();
			return;
		}
		if ( focusSlideIndex > slideIndex ) {
			setFocusSlideIndex( focusSlideIndex - 1 );
		}
	};

	const reorderSlides = ( nextSlides, { from, to } = {} ) => {
		setAttributes( { slides: nextSlides } );
		if ( from == null || to == null ) {
			return;
		}
		const nextFocus = remapFocusSlideIndex( focusSlideIndex, from, to );
		if ( nextFocus !== focusSlideIndex ) {
			setFocusSlideIndex( nextFocus );
		}
		if ( nextFocus != null ) {
			focusBmSliderSlide( nextFocus );
		}
	};

	const unlockInstanceStyles = () => {
		onEnsureSidebar?.();
		setStyleScopeChoice( 'instance' );
	};

	const showChoiceGate =
		enableStyleScopeGate && ! independentInstance;
	const stylesLocked = showChoiceGate;
	const scopeGateNode = showChoiceGate
		? createElement( StyleScopeGate, {
				onCustomizeInstance: unlockInstanceStyles,
				editMyBlockUrl: myBlockEditorUrl( meta?.id ),
		  } )
		: null;

	// Single-element blocks (e.g. Button) — show Design controls without a click.
	useEffect( () => {
		if ( ! isSingleStyleable ) {
			return;
		}
		const onlyKey = styleable[ 0 ]?.key;
		if ( ! onlyKey || selectedElementKey === onlyKey ) {
			return;
		}
		if ( typeof onSelectElement === 'function' ) {
			onSelectElement( onlyKey );
		}
	}, [ isSingleStyleable, styleable, selectedElementKey, onSelectElement ] );

	const lib = useButtonLibrary( { refreshOnMount: true, refreshOnFocus: true } );

	const slidesOrderField = useMemo( () => {
		const slidesField = ( meta.fieldSchema || [] ).find(
			( field ) =>
				field?.key === 'slides' && Array.isArray( field.fields )
		);
		return (
			( slidesField?.fields || [] ).find(
				( field ) => field?.type === 'elementsOrder'
			) || null
		);
	}, [ meta.fieldSchema ] );

	const visibilityKeys = useMemo( () => {
		const orderField =
			slidesOrderField ||
			( meta.fieldSchema || [] ).find(
				( field ) => field?.type === 'elementsOrder'
			) ||
			{};
		const defaultAttrs = activeSlide
			? ( Array.isArray( meta.defaultAttrs?.slides ) &&
					meta.defaultAttrs.slides[ 0 ] ) ||
			  activeSlide
			: meta.defaultAttrs || {};
		return visibilityKeysForField( orderField, defaultAttrs );
	}, [
		meta.fieldSchema,
		meta.defaultAttrs,
		slidesOrderField,
		activeSlide,
	] );

	const layoutMedia = useMemo(
		() =>
			layoutMediaFields(
				meta.fieldSchema || [],
				elementsOrderDefaults
			),
		[ meta.fieldSchema, elementsOrderDefaults ]
	);

	const contentField = useMemo( () => {
		if ( ! selectedElementKey ) {
			return null;
		}
		const schema = meta.fieldSchema || [];
		const mergeTwinFlags = ( nested ) => {
			const top = schema.find(
				( field ) => field?.key === selectedElementKey
			);
			if ( ! top || ! nested ) {
				return nested;
			}
			return {
				...nested,
				disableAdvanceEditor:
					nested.disableAdvanceEditor ??
					nested.disableAdvancedEditor ??
					top.disableAdvanceEditor ??
					top.disableAdvancedEditor,
				allowedCommand:
					nested.allowedCommand ??
					nested.allowedCommands ??
					top.allowedCommand ??
					top.allowedCommands,
			};
		};
		// Prefer nested slide field schema when editing a slide element.
		if ( activeSlide ) {
			const slidesField = schema.find(
				( field ) =>
					field?.key === 'slides' && Array.isArray( field.fields )
			);
			const nested = ( slidesField?.fields || [] ).find(
				( field ) =>
					field?.key === selectedElementKey &&
					field?.type !== 'elementsOrder'
			);
			if ( nested ) {
				return mergeTwinFlags( nested );
			}
		}
		// Prefer nested collection item field when a canvas item is focused
		// (styles stay shared; content remains per-item).
		if ( activeCard && collectionField ) {
			const nested = ( collectionField.fields || [] ).find(
				( field ) =>
					field?.key === selectedElementKey &&
					field?.type !== 'elementsOrder'
			);
			if ( nested ) {
				return mergeTwinFlags( nested );
			}
			// Collection card focused but only a styleOnly catalog twin exists.
			const top = schema.find(
				( field ) =>
					field?.key === selectedElementKey &&
					field?.type !== 'elementsOrder'
			);
			if ( top ) {
				return { ...top, styleOnly: false };
			}
		}
		return (
			schema.find(
				( field ) =>
					field?.key === selectedElementKey &&
					field?.type !== 'elementsOrder' &&
					! field?.styleOnly
			) ||
			( () => {
				// Style-only catalog twins (card title/description/buttons) still
				// need a Content row so hide/show can appear in Customize.
				const top = schema.find(
					( field ) =>
						field?.key === selectedElementKey &&
						field?.type !== 'elementsOrder'
				);
				if ( top && visibilityKeys.includes( String( top.key || '' ) ) ) {
					return { ...top, styleOnly: false };
				}
				return null;
			} )()
		);
	}, [
		meta.fieldSchema,
		selectedElementKey,
		activeSlide,
		activeCard,
		collectionField,
		visibilityKeys,
	] );

	const setContentAttributes = ( partial ) => {
		if ( activeSlide && focusSlideIndex != null ) {
			const nextSlides = slides.map( ( slide, i ) =>
				i === focusSlideIndex ? { ...slide, ...partial } : slide
			);
			setAttributes( { slides: nextSlides } );
			return;
		}
		if ( collectionKey && collectionItems.length ) {
			let idx =
				focusCardIndex != null && collectionItems[ focusCardIndex ]
					? Number( focusCardIndex )
					: null;
			if ( idx == null ) {
				const fromAttr = Number( attributes?.activeTab );
				idx =
					Number.isFinite( fromAttr ) && collectionItems[ fromAttr ]
						? fromAttr
						: 0;
			}
			const keys = Object.keys( partial || {} );
			const writeToItem =
				( focusCardIndex != null &&
					collectionItems[ focusCardIndex ] ) ||
				keys.includes( 'icon' ) ||
				keys.includes( 'iconSize' ) ||
				keys.includes( 'iconColor' ) ||
				keys.some( ( k ) =>
					[
						'accordionTitle',
						'accordionContent',
						'tabTitle',
						'tabContent',
						'title',
						'header',
						'content',
						'text',
						'buttons',
						'link',
						'image',
					].includes( k )
				);
			if ( writeToItem && collectionItems[ idx ] ) {
				if (
					collectionSharesIcon( collectionField ) &&
					partialTouchesSharedIcon( partial )
				) {
					const shared = pickSharedIconPartial( partial );
					const rest = { ...( partial || {} ) };
					Object.keys( shared ).forEach( ( k ) => {
						delete rest[ k ];
					} );
					const hasRest = Object.keys( rest ).length > 0;
					const nextItems = collectionItems.map( ( card, i ) => {
						const next = { ...card, ...shared };
						return i === idx && hasRest
							? { ...next, ...rest }
							: next;
					} );
					setAttributes( { [ collectionKey ]: nextItems } );
					return;
				}
				const nextItems = collectionItems.map( ( card, i ) =>
					i === idx ? { ...card, ...partial } : card
				);
				setAttributes( { [ collectionKey ]: nextItems } );
				return;
			}
		}
		setAttributes( partial );
	};

	const isColumnsLayout = useMemo( () => {
		if ( activeSlide && hasSlideElements ) {
			return isColumnsSlideLayout( activeSlide.layout );
		}
		const fromVars = String(
			effective( cssVars, 'desktop' )[ '--bm-columns-breakpoint' ] ?? ''
		).trim();
		if ( fromVars ) {
			return true;
		}
		return ( meta.styleSchema || [] ).some( ( group ) =>
			( group?.fields || [] ).some(
				( field ) =>
					field?.key === '--bm-columns-breakpoint' ||
					field?.type === 'columnsBreakpoint'
			)
		);
	}, [ activeSlide, hasSlideElements, cssVars, meta.styleSchema ] );

	const visibilityKeyBreakpoints = useMemo(
		() => columnMediaVisibilityKeyBreakpoints( isColumnsLayout ),
		[ isColumnsLayout ]
	);

	const visibilitySourceAttrs =
		activeSlide || ( hasCollectionElements ? attributes : null ) || attributes;
	const {
		visibility,
		visibilityBreakpoint,
		onVisibilityChange,
	} = contentFieldVisibility(
		meta,
		visibilitySourceAttrs,
		// Card collections share visibility across items; only slides are per-item.
		activeSlide ? setContentAttributes : setAttributes,
		breakpoint,
		activeSlide
			? {
					orderField: slidesOrderField,
					defaultAttrs:
						( Array.isArray( meta.defaultAttrs?.slides ) &&
							meta.defaultAttrs.slides[ 0 ] ) ||
						activeSlide,
			  }
			: {}
	);

	const isEditingCollectionItem =
		selectedField?.elementType === 'tabLabel' ||
		selectedElementKey === 'tabTitle' ||
		selectedElementKey === 'accordionTitle';

	const removeCardIndex =
		focusCardIndex != null && collectionItems[ focusCardIndex ]
			? Number( focusCardIndex )
			: isEditingCollectionItem && collectionItems.length
				? ( () => {
						const fromAttr = Number( attributes?.activeTab );
						if (
							Number.isFinite( fromAttr ) &&
							collectionItems[ fromAttr ]
						) {
							return fromAttr;
						}
						return 0;
				  } )()
				: null;

	const contentSourceAttrs =
		activeSlide ||
		( removeCardIndex != null
			? collectionItems[ removeCardIndex ]
			: null ) ||
		activeCard ||
		attributes;
	const canEditButtonStyles =
		typeof onEditButtonStyles === 'function' &&
		( ! enableStyleScopeGate || independentInstance );
	const contentEditor = contentField
		? createElement( ContentFieldEditor, {
				field: contentField,
				attributes: contentSourceAttrs,
				setAttributes: setContentAttributes,
				visibilityKeys,
				visibility,
				visibilityBreakpoint,
				onVisibilityChange,
				visibilityKeyBreakpoints,
				onEditButtonStyles: canEditButtonStyles
					? onEditButtonStyles
					: undefined,
				renderRichText,
				allowManageButtonItems: Boolean( activeCard || activeSlide ),
				previewScope: {
					cardIndex:
						removeCardIndex != null
							? removeCardIndex
							: activeCard
								? focusCardIndex
								: null,
					slideIndex: activeSlide ? focusSlideIndex : null,
				},
				fieldSchema:
					( activeSlide
						? ( meta.fieldSchema || [] ).find(
								( field ) => field?.key === 'slides'
						  )?.fields
						: null ) ||
					( activeCard || removeCardIndex != null
						? collectionField?.fields
						: null ) ||
					meta.fieldSchema ||
					[],
			} )
		: null;

	const canRemoveCardForToolbar =
		( ! enableStyleScopeGate || independentInstance ) &&
		collectionItemChromeActionsAllowed(
			collectionField || {},
			selectedElementKey
		) &&
		removeCardIndex != null &&
		collectionKey &&
		collectionItems.length >
			collectionMinItems( collectionField || {} );

	const removeCardLabel = String(
		( removeCardIndex != null
			? collectionItems[ removeCardIndex ]
			: activeCard
		)?.tabTitle ||
			( removeCardIndex != null
				? collectionItems[ removeCardIndex ]
				: activeCard
			)?.accordionTitle ||
			( removeCardIndex != null
				? collectionItems[ removeCardIndex ]
				: activeCard
			)?.title ||
			( removeCardIndex != null
				? collectionItems[ removeCardIndex ]
				: activeCard
			)?.header ||
			''
	)
		.replace( /<[^>]+>/g, ' ' )
		.replace( /\s+/g, ' ' )
		.trim();

	const removeFocusedCard = () => {
		if ( ! canRemoveCardForToolbar || removeCardIndex == null ) {
			return;
		}
		const removedIndex = Number( removeCardIndex );
		const nextItems = collectionItems.filter(
			( _, i ) => i !== removedIndex
		);
		const patch = { [ collectionKey ]: nextItems };
		const active = Number( attributes?.activeTab );
		if ( Number.isFinite( active ) ) {
			if ( ! nextItems.length ) {
				patch.activeTab = 0;
			} else if (
				active >= nextItems.length ||
				active === removedIndex
			) {
				patch.activeTab = Math.min(
					removedIndex,
					nextItems.length - 1
				);
			} else if ( active > removedIndex ) {
				patch.activeTab = active - 1;
			}
		}
		setAttributes( patch );
		if ( ! nextItems.length ) {
			setFocusCardIndex( null );
			clearElement();
			return;
		}
		const nextFocus = Math.min( removedIndex, nextItems.length - 1 );
		setFocusCardIndex( nextFocus );
	};

	const setVar = ( key, value, forceBreakpoint ) => {
		if ( enableStyleScopeGate && ! independentInstance ) {
			return;
		}
		const instanceVars = attributes.cssVars || {};
		setAttributes( {
			cssVars:
				forceBreakpoint === 'desktop'
					? setSharedDesktopVar( instanceVars, key, value )
					: setBreakpointVar( instanceVars, breakpoint, key, value ),
		} );
	};

	const setVarsPatch = ( patch, forceBreakpoint ) => {
		if ( enableStyleScopeGate && ! independentInstance ) {
			return;
		}
		let next = attributes.cssVars || {};
		Object.entries( patch || {} ).forEach( ( [ key, value ] ) => {
			next =
				forceBreakpoint === 'desktop'
					? setSharedDesktopVar( next, key, value )
					: setBreakpointVar( next, breakpoint, key, value );
		} );
		setAttributes( { cssVars: next } );
	};

	const updateActiveSlideCssVars = ( updater ) => {
		if ( focusSlideIndex == null || ! activeSlide ) {
			return;
		}
		const hydrated = ensureSlideLayoutCssVars( activeSlide, cssVars );
		const nextVars = updater( hydrated );
		const nextSlides = slides.map( ( slide, i ) =>
			i === focusSlideIndex ? { ...slide, cssVars: nextVars } : slide
		);
		setAttributes( { slides: nextSlides } );
	};

	const setSlideVar = ( key, value, forceBreakpoint ) => {
		updateActiveSlideCssVars( ( prev ) =>
			forceBreakpoint === 'desktop'
				? setSharedDesktopVar( prev, key, value )
				: setBreakpointVar( prev, breakpoint, key, value )
		);
	};

	const setSlideVarsPatch = ( patch, forceBreakpoint ) => {
		updateActiveSlideCssVars( ( prev ) => {
			let next = prev;
			Object.entries( patch || {} ).forEach( ( [ key, value ] ) => {
				next =
					forceBreakpoint === 'desktop'
						? setSharedDesktopVar( next, key, value )
						: setBreakpointVar( next, breakpoint, key, value );
			} );
			return next;
		} );
	};

	const activeSlideLayoutValues = useMemo(
		() =>
			activeSlide
				? effectiveSlideLayoutValues( activeSlide, cssVars, breakpoint )
				: {},
		[ activeSlide, cssVars, breakpoint ]
	);

	const activeMedia = useMemo( () => {
		if ( activeSlide && hasSlideElements ) {
			return activeSlide.image;
		}
		return attributes.image;
	}, [ activeSlide, hasSlideElements, attributes.image ] );

	const showMediaFullscreen =
		isColumnsLayout && mediaAllowsFullscreenFrame( activeMedia );

	const mediaFrameValue = useMemo( () => {
		if ( activeSlide && hasSlideElements && isColumnsLayout ) {
			return (
				activeSlideLayoutValues[ '--bm-media-frame' ] ??
				activeVars[ '--bm-media-frame' ] ??
				'framed'
			);
		}
		return activeVars[ '--bm-media-frame' ] ?? 'framed';
	}, [
		activeSlide,
		hasSlideElements,
		isColumnsLayout,
		activeSlideLayoutValues,
		activeVars,
	] );

	const activeSlideStyleGroups = useMemo( () => {
		if ( ! activeSlide ) {
			return [];
		}
		return slideStyleGroupsFor( activeSlide.layout, breakpoint );
	}, [ activeSlide, breakpoint ] );

	const onMediaFrameChange = ( next ) => {
		if ( activeSlide && hasSlideElements && isColumnsLayout ) {
			setSlideVar( '--bm-media-frame', next );
			return;
		}
		setVar( '--bm-media-frame', next );
	};

	// Embeds cannot use fullscreen fill — force framed when media type is iframe.
	useEffect( () => {
		if (
			! isColumnsLayout ||
			mediaAllowsFullscreenFrame( activeMedia ) ||
			! isMediaFullscreen( mediaFrameValue )
		) {
			return;
		}
		onMediaFrameChange( 'framed' );
		// eslint-disable-next-line react-hooks/exhaustive-deps -- only when media kind / frame mode conflict
	}, [ isColumnsLayout, activeMedia, mediaFrameValue ] );

	const bpButtons = [
		{ id: 'desktop', label: __( 'Desktop', 'joc-blockora-morph' ), icon: 'desktop' },
		{ id: 'tablet', label: __( 'Tablet', 'joc-blockora-morph' ), icon: 'tablet' },
		{ id: 'mobile', label: __( 'Mobile', 'joc-blockora-morph' ), icon: 'smartphone' },
	];

	const breakpointBar = createElement(
		'div',
		{ className: 'bm-gutenberg-breakpoints-bar' },
		createElement(
			'div',
			{
				className: 'bm-gutenberg-breakpoints',
				role: 'group',
				'aria-label': __( 'Breakpoint', 'joc-blockora-morph' ),
			},
			bpButtons.map( ( bp ) =>
				createElement(
					'button',
					{
						key: bp.id,
						type: 'button',
						className:
							'bm-gutenberg-seg-btn' +
							( breakpoint === bp.id ? ' is-active' : '' ),
						'aria-pressed': breakpoint === bp.id,
						onClick: () => {
							if ( breakpoint === bp.id ) {
								return;
							}
							onViewSwitch?.();
							setBreakpoint( bp.id );
							requestAnimationFrame( () => {
								scrollInspectorToTop();
							} );
						},
					},
					createElement( 'span', {
						className: `dashicons dashicons-${ bp.icon }`,
						'aria-hidden': true,
					} ),
					createElement( 'span', null, bp.label )
				)
			)
		),
		createElement(
			'p',
			{ className: 'bm-gutenberg-breakpoint-hint' },
			breakpointHints === 'frontend'
				? breakpoint === 'desktop'
					? __(
							'Desktop — full page width.',
							'joc-blockora-morph'
					  )
					: breakpoint === 'tablet'
					? __(
							'Tablet — page body capped at 1024px (window stays full size).',
							'joc-blockora-morph'
					  )
					: __(
							'Mobile — page body capped at 480px (window stays full size).',
							'joc-blockora-morph'
					  )
				: breakpoint === 'desktop'
				? __(
						'Editing Desktop styles (follows WordPress preview).',
						'joc-blockora-morph'
				  )
				: breakpoint === 'tablet'
				? __(
						'Editing Tablet overrides — synced with WordPress tablet preview.',
						'joc-blockora-morph'
				  )
				: __(
						'Editing Mobile overrides — synced with WordPress mobile preview.',
						'joc-blockora-morph'
				  )
		)
	);

	const styleGroupsNode = groups.length
		? groups.map( ( group ) => {
				const fields = ( group.fields || [] ).filter( ( f ) =>
					isStyleFieldVisibleAtBreakpoint( f, breakpoint )
				);
				if ( ! fields.length ) {
					return null;
				}
				return createElement(
					'div',
					{
						key: group.group,
						className: 'bm-inspector-card bm-gutenberg-style-group',
					},
					createElement(
						'strong',
						{ className: 'bm-inspector-card__title' },
						group.group
					),
					createElement( StyleFieldList, {
						fields,
						values: activeVars,
						onVarChange: setVar,
						onVarsPatch: setVarsPatch,
						breakpoint,
					} )
				);
		  } )
		: createElement(
				'p',
				null,
				__( 'No style settings for this block.', 'joc-blockora-morph' )
		  );

	const sliderSettingsNode = sliderSettingsFields.length
		? createElement(
				'div',
				{ className: 'bm-inspector-card' },
				createElement(
					'strong',
					{ className: 'bm-inspector-card__title' },
					__( 'Slider', 'joc-blockora-morph' )
				),
				...sliderSettingsFields.map( ( field ) =>
					createElement( ContentFieldEditor, {
						key: field.key,
						field,
						attributes,
						setAttributes,
						renderRichText,
						fieldSchema: meta.fieldSchema || [],
					} )
				)
		  )
		: null;

	const layoutMediaNode = layoutMedia.length
		? createElement(
				'div',
				{ className: 'bm-inspector-card' },
				createElement(
					'strong',
					{ className: 'bm-inspector-card__title' },
					__( 'Media', 'joc-blockora-morph' )
				),
				...layoutMedia.map( ( field ) =>
					createElement( ContentFieldEditor, {
						key: field.key,
						field,
						attributes,
						setAttributes,
						visibilityKeys,
						visibility,
						visibilityBreakpoint,
						onVisibilityChange,
						visibilityKeyBreakpoints,
						fieldSchema: meta.fieldSchema || [],
						allowManageButtonItems: false,
					} )
				)
		  )
		: null;

	const designSubTabBar = ( hasSlideElements || hasCollectionElements )
		? createElement(
				'div',
				{
					className: 'bm-design-subtabs',
					role: 'tablist',
					'aria-label': __(
						'Customization sections',
						'joc-blockora-morph'
					),
				},
				hasSlideElements ? createElement(
					'button',
					{
						type: 'button',
						role: 'tab',
						'aria-selected': designSubTab === 'slides',
						className:
							'bm-design-subtabs__btn' +
							( designSubTab === 'slides' ? ' is-active' : '' ),
						onClick: () => setDesignSubTab( 'slides' ),
					},
					__( 'Slides', 'joc-blockora-morph' )
				) : null,
				hasCollectionElements ? createElement(
					'button',
					{
						type: 'button',
						role: 'tab',
						'aria-selected': designSubTab === 'items',
						className:
							'bm-design-subtabs__btn' +
							( designSubTab === 'items' ? ' is-active' : '' ),
						onClick: () => setDesignSubTab( 'items' ),
					},
					itemsTabLabel
				) : null,
				createElement(
					'button',
					{
						type: 'button',
						role: 'tab',
						'aria-selected': designSubTab === 'general',
						className:
							'bm-design-subtabs__btn' +
							( designSubTab === 'general' ? ' is-active' : '' ),
						onClick: () => setDesignSubTab( 'general' ),
					},
					__( 'General', 'joc-blockora-morph' )
				)
		  )
		: null;

	return createElement(
		Fragment,
		null,
		breakpointBar,
		selectedField
			? createElement( ElementToolbar, {
					field: selectedField,
					values: activeVars,
					onVarChange: setVar,
					onVarsPatch: setVarsPatch,
					breakpoint,
					onBack: isSingleStyleable ? undefined : clearElement,
					buttonLibrary: lib,
					buttons: toolbarButtons,
					onButtonsChange: onToolbarButtonsChange,
					allowManageButtons: Boolean( activeCard || activeSlide ),
					focusButtonIndex,
					contentEditor,
					contentAttrs:
						selectedField?.key === 'accordionArrow' ||
						selectedField?.elementType === 'accordionArrow'
							? attributes
							: contentSourceAttrs,
					onContentAttrsChange:
						selectedField?.key === 'accordionArrow' ||
						selectedField?.elementType === 'accordionArrow'
							? setAttributes
							: setContentAttributes,
					sharedIcon: collectionSharesIcon( collectionField ),
					stylesGate: scopeGateNode,
					stylesLocked,
					elementVisible: selectedField
						? isElementVisible(
								selectedField.key,
								activeSlide ||
									( hasCollectionElements
										? attributes
										: activeCard ) ||
									attributes,
								breakpoint
						  )
						: true,
					slideIndex: hasSlideElements ? focusSlideIndex : null,
					showMediaFullscreen,
					mediaFrameValue,
					onMediaFrameChange,
					onRemoveCard: canRemoveCardForToolbar
						? removeFocusedCard
						: undefined,
					canRemoveCard: canRemoveCardForToolbar,
					removeCardLabel,
			  } )
			: createElement(
					'div',
					{
						className:
							'bm-inspector-stack bm-style-scope-stack' +
							( stylesLocked ? ' is-gated' : '' ),
					},
					scopeGateNode,
					designSubTabBar,
					hasSlideElements && designSubTab === 'general'
						? createElement(
								Fragment,
								null,
								sliderSettingsNode,
								layoutMediaNode,
								styleGroupsNode
						  )
						: null,
					hasCollectionElements && designSubTab === 'general'
						? createElement(
								Fragment,
								null,
								layoutMediaNode,
								styleGroupsNode
						  )
						: null,
					hasSlideElements && designSubTab === 'slides'
						? createElement(
								'div',
								{
									className:
										'bm-inspector-card bm-gutenberg-elements is-open',
								},
								focusSlideIndex != null && activeSlide
									? createElement(
											'div',
											{
												className:
													'bm-gutenberg-elements__slide-nav',
											},
											createElement(
												Button,
												{
													variant: 'tertiary',
													className:
														'bm-gutenberg-elements__back',
													icon: 'arrow-left-alt2',
													onClick: leaveSlide,
												},
												__( 'Slides', 'joc-blockora-morph' )
											),
											createElement(
												'div',
												{
													className:
														'bm-gutenberg-elements__slide-nav-title',
												},
												createElement(
													'strong',
													{
														className:
															'bm-inspector-card__title',
													},
													sprintf(
														/* translators: %d: slide number */
														__(
															'Slide %d',
															'joc-blockora-morph'
														),
														focusSlideIndex + 1
													)
												),
												createElement(
													'span',
													{
														className:
															'bm-gutenberg-elements__slide-layout',
													},
													slideLayoutLabel(
														activeSlide.layout
													)
												)
											),
											canRemoveSlide
												? createElement( Button, {
														size: 'small',
														variant: 'tertiary',
														isDestructive: true,
														className:
															'bm-gutenberg-elements__slide-remove',
														icon: 'no-alt',
														onClick: () =>
															removeSlide(
																focusSlideIndex
															),
														'aria-label': __(
															'Remove slide',
															'joc-blockora-morph'
														),
												  } )
												: null
									  )
									: createElement(
											'strong',
											{
												className:
													'bm-inspector-card__title',
											},
											__( 'Slides', 'joc-blockora-morph' )
									  ),
								createElement(
									'div',
									{
										className:
											'bm-gutenberg-elements__list',
									},
									focusSlideIndex != null && activeSlide
										? createElement(
												Fragment,
												null,
												createElement(
													'strong',
													{
														className:
															'bm-gutenberg-elements__section-label',
													},
													__(
														'Elements',
														'joc-blockora-morph'
													)
												),
												allowElementReorder
													? createElement(
															'p',
															{
																className:
																	'bm-gutenberg-elements__order-hint',
															},
															sprintf(
																/* translators: %s: desktop, tablet, or mobile */
																__(
																	'Drag to a drop line between items for %s. Each viewport can have its own order.',
																	'joc-blockora-morph'
																),
																breakpoint ===
																	'tablet'
																	? __(
																			'Tablet',
																			'joc-blockora-morph'
																	  )
																	: breakpoint ===
																	  'mobile'
																	? __(
																			'Mobile',
																			'joc-blockora-morph'
																	  )
																	: __(
																			'Desktop',
																			'joc-blockora-morph'
																	  )
															)
													  )
													: null,
												createElement(
													'div',
													{
														className:
															allowElementReorder
																? 'bm-gutenberg-elements__sortable'
																: 'bm-gutenberg-elements__list-static',
													},
													createElement(
														SlideElementsList,
														{
															styleable,
															slide: activeSlide,
															attributes:
																activeSlide ||
																attributes,
															breakpoint,
															orderDefaults:
																slideOrderDefaults(
																	activeSlide?.layout
																),
															listKey: String(
																focusSlideIndex
															),
															selectedKey:
																selectedElementKey,
															allowReorder:
																allowElementReorder,
															onSelect: (
																fieldKey
															) =>
																selectSlideElement(
																	focusSlideIndex,
																	fieldKey
																),
															onReorder:
																allowElementReorder
																	? (
																			nextOrder
																	  ) => {
																			const orderDefaults =
																				slideOrderDefaults(
																					activeSlide?.layout
																				);
																			const nextSlides =
																				slides.map(
																					(
																						slide,
																						i
																					) =>
																						i ===
																						focusSlideIndex
																							? {
																									...slide,
																									elementsOrder:
																										serializeElementsOrder(
																											setElementsOrderAtBreakpoint(
																												slide.elementsOrder,
																												breakpoint,
																												nextOrder,
																												orderDefaults
																											)
																										),
																							  }
																							: slide
																				);
																			setAttributes(
																				{
																					slides: nextSlides,
																				}
																			);
																	  }
																	: undefined,
														}
													)
												),
												activeSlideStyleGroups.length
													? createElement(
															'div',
															{
																className:
																	'bm-gutenberg-elements__slide-layout-styles',
															},
															activeSlideStyleGroups.map(
																( group ) =>
																	createElement(
																		'div',
																		{
																			key: group.group,
																			className:
																				'bm-gutenberg-elements__slide-style-group',
																		},
																		createElement(
																			'strong',
																			{
																				className:
																					'bm-gutenberg-elements__section-label',
																			},
																			group.group
																		),
																		createElement(
																			StyleFieldList,
																			{
																				fields:
																					group.fields,
																				values: activeSlideLayoutValues,
																				onVarChange:
																					setSlideVar,
																				onVarsPatch:
																					setSlideVarsPatch,
																				breakpoint,
																			}
																		)
																	)
															)
													  )
													: null
										  )
										: createElement(
												Fragment,
												null,
												createElement( SlidesList, {
													slides,
													onOpen: openSlide,
													onRemove: removeSlide,
													onReorder: reorderSlides,
												} ),
												createElement(
													'div',
													{
														className:
															'bm-gutenberg-elements__add-slide-row',
													},
													createElement(
														SelectControl,
														{
															label: __(
																'Hero layout',
																'joc-blockora-morph'
															),
															value: addSlideLayout,
															options:
																SLIDE_LAYOUT_OPTIONS.map(
																	( opt ) => ( {
																		value: opt.value,
																		label: __(
																			opt.label,
																			'joc-blockora-morph'
																		),
																	} )
																),
															onChange: ( next ) =>
																setAddSlideLayout(
																	next ||
																		'simple'
																),
															__nextHasNoMarginBottom: true,
															__next40pxDefaultSize: true,
														}
													),
													createElement(
														Button,
														{
															variant: 'secondary',
															className:
																'bm-gutenberg-elements__add-slide',
															icon: 'plus-alt2',
															onClick: addSlide,
														},
														__(
															'Add slide',
															'joc-blockora-morph'
														)
													)
												)
										  )
								)
						  )
						: null,
					hasCollectionElements && designSubTab === 'items'
						? createElement(
								'div',
								{
									className:
										'bm-inspector-card bm-gutenberg-elements is-open',
								},
								createElement(
									'strong',
									{
										className: 'bm-inspector-card__title',
									},
									__( 'Elements', 'joc-blockora-morph' )
								),
								createElement(
									'div',
									{
										className:
											'bm-gutenberg-elements__list',
									},
									styleable.length
										? createElement(
												Fragment,
												null,
												createElement(
													'p',
													{
														className:
															'bm-gutenberg-elements__order-hint',
													},
													__(
														'Styles apply to every item. Add or edit item content in the Fields tab.',
														'joc-blockora-morph'
													)
												),
												allowCollectionElementReorder
													? createElement(
															'p',
															{
																className:
																	'bm-gutenberg-elements__order-hint',
															},
															sprintf(
																/* translators: %s: desktop, tablet, or mobile */
																__(
																	'Drag to a drop line between items for %s. Each viewport can have its own order.',
																	'joc-blockora-morph'
																),
																breakpoint ===
																	'tablet'
																	? __(
																			'Tablet',
																			'joc-blockora-morph'
																	  )
																	: breakpoint ===
																	  'mobile'
																		? __(
																				'Mobile',
																				'joc-blockora-morph'
																		  )
																		: __(
																				'Desktop',
																				'joc-blockora-morph'
																		  )
															)
													  )
													: null,
												createElement(
													'div',
													{
														className:
															allowCollectionElementReorder
																? 'bm-gutenberg-elements__sortable'
																: 'bm-gutenberg-elements__list-static',
													},
													createElement(
														SlideElementsList,
														{
															fields: styleable,
															attributes,
															breakpoint,
															elementsOrder:
																attributes.elementsOrder,
															orderDefaults:
																elementsOrderDefaults,
															listKey: 'collection',
															selectedKey:
																selectedElementKey,
															staticKeys:
																allowCollectionElementReorder
																	? [ 'button' ]
																	: undefined,
															allowReorder:
																allowCollectionElementReorder,
															onSelect:
																selectCollectionElement,
															onReorder:
																allowCollectionElementReorder
																	? (
																			nextOrder
																	  ) => {
																			setAttributes(
																				{
																					elementsOrder:
																						serializeElementsOrder(
																							setElementsOrderAtBreakpoint(
																								attributes.elementsOrder,
																								breakpoint,
																								nextOrder,
																								elementsOrderDefaults
																							)
																						),
																				}
																			);
																	  }
																	: undefined,
														}
													)
												)
										  )
										: createElement(
												'p',
												{
													className:
														'bm-gutenberg-elements__order-hint',
												},
												__(
													'No styleable elements. Use General for block styles, or Fields to manage items.',
													'joc-blockora-morph'
												)
										  )
								)
						  )
						: null,
					! hasSlideElements && ! hasCollectionElements
						? createElement(
								Fragment,
								null,
								sliderSettingsNode,
								styleable.length
									? createElement(
											'div',
											{
												className:
													'bm-inspector-card bm-gutenberg-elements' +
													( elementsExpanded
														? ' is-open'
														: '' ),
											},
											createElement(
												'button',
												{
													type: 'button',
													className:
														'bm-inspector-card__toggle',
													'aria-expanded':
														elementsExpanded,
													onClick: () =>
														setElementsExpanded(
															! elementsExpanded
														),
												},
												createElement(
													'strong',
													{
														className:
															'bm-inspector-card__title',
													},
													__(
														'Elements',
														'joc-blockora-morph'
													)
												),
												createElement( 'span', {
													className:
														'dashicons dashicons-arrow-down-alt2 bm-inspector-card__chevron',
													'aria-hidden': true,
												} )
											),
											elementsExpanded
												? createElement(
														'div',
														{
															className:
																'bm-gutenberg-elements__list',
														},
														allowElementReorder
															? createElement(
																	'p',
																	{
																		className:
																			'bm-gutenberg-elements__order-hint',
																	},
																	sprintf(
																		/* translators: %s: desktop, tablet, or mobile */
																		__(
																			'Drag to a drop line between items for %s. Each viewport can have its own order.',
																			'joc-blockora-morph'
																		),
																		breakpoint ===
																			'tablet'
																			? __(
																					'Tablet',
																					'joc-blockora-morph'
																			  )
																			: breakpoint ===
																			  'mobile'
																			? __(
																					'Mobile',
																					'joc-blockora-morph'
																			  )
																			: __(
																					'Desktop',
																					'joc-blockora-morph'
																			  )
																	)
															  )
															: null,
														createElement(
															'div',
															{
																className:
																	allowElementReorder
																		? 'bm-gutenberg-elements__sortable'
																		: 'bm-gutenberg-elements__list-static',
															},
															createElement(
																SlideElementsList,
																{
																	fields: styleable.filter(
																		(
																			field
																		) =>
																			elementsOrderDefaults.includes(
																				field.key
																			)
																	),
																	attributes,
																	breakpoint,
																	elementsOrder:
																		attributes.elementsOrder,
																	orderDefaults:
																		elementsOrderDefaults,
																	listKey:
																		'block',
																	selectedKey:
																		selectedElementKey,
																	allowReorder:
																		allowElementReorder,
																	onSelect: (
																		fieldKey
																	) =>
																		selectElement(
																			fieldKey
																		),
																	onReorder:
																		allowElementReorder
																			? (
																					nextOrder
																			  ) => {
																					setAttributes(
																						{
																							elementsOrder:
																								serializeElementsOrder(
																									setElementsOrderAtBreakpoint(
																										attributes.elementsOrder,
																										breakpoint,
																										nextOrder,
																										elementsOrderDefaults
																									)
																								),
																						}
																					);
																			  }
																			: undefined,
																}
															)
														),
														( () => {
															const extras =
																styleable.filter(
																	( field ) =>
																		! elementsOrderDefaults.includes(
																			field.key
																		)
																);
															if (
																! extras.length
															) {
																return null;
															}
															return createElement(
																'div',
																{
																	className:
																		'bm-gutenberg-elements__list-static',
																},
																...extras.map(
																	(
																		field,
																		extraIndex
																	) =>
																		ElementPickerButton(
																			field,
																			{
																				attributes,
																				breakpoint,
																				index:
																					styleable.filter(
																						(
																							f
																						) =>
																							elementsOrderDefaults.includes(
																								f.key
																							)
																					)
																						.length +
																					extraIndex +
																					1,
																				selected:
																					selectedElementKey ===
																					field.key,
																				onClick:
																					() =>
																						selectElement(
																							field.key
																						),
																			}
																		)
																)
															);
														} )()
												  )
												: null
									  )
									: null,
								layoutMediaNode,
								styleGroupsNode
						  )
						: null
			  )
	);
}

/**
 * Element order controls (admin Block Editor + shortcode Content Editor).
 */
export function OrderControls( {
	meta,
	attributes,
	setAttributes,
	appliesGlobally = true,
} ) {
	const orderFields = ( meta.fieldSchema || [] ).filter(
		( field ) => field?.type === 'elementsOrder'
	);

	if ( ! orderFields.length ) {
		return createElement(
			'p',
			null,
			__( 'No reorderable elements for this block.', 'joc-blockora-morph' )
		);
	}

	return createElement(
		'div',
		{ className: 'bm-inspector-stack' },
		createElement(
			'div',
			{ className: 'bm-inspector-card' },
			createElement(
				'strong',
				{ className: 'bm-inspector-card__title' },
				__( 'Element order', 'joc-blockora-morph' )
			),
			...orderFields.map( ( field ) =>
				createElement( ElementsOrderField, {
					key: field.key,
					label: '',
					value: attributes[ field.key ],
					onChange: ( next ) =>
						setAttributes( { [ field.key ]: next } ),
					field,
					appliesGlobally,
				} )
			)
		)
	);
}

/**
 * Fields / Customization tab panel — shared between Gutenberg inspector and FE sidebar.
 * Optional Order tab matches the admin block editor.
 */
export default function BlockInspectorPanel( {
	meta,
	attributes,
	setAttributes,
	selectedElementKey = null,
	onSelectElement,
	onClearElement,
	initialTabName = 'fields',
	tabPanelKey = 0,
	onTabSelect,
	onEditButtonStyles,
	focusButtonIndex = null,
	styleBreakpoint,
	onStyleBreakpointChange,
	onViewSwitch,
	buttonLibrary,
	renderRichText,
	className = 'bm-gutenberg-tabs',
	breakpointHints = 'editor',
	showOrderTab = false,
	orderAppliesGlobally = true,
	allowElementReorder = false,
	enableStyleScopeGate = false,
	libraryCssVars,
	onEnsureSidebar,
	styleScopeSurfaceId = '',
	focusSlideIndex = null,
	onFocusSlideIndexChange,
	focusCardIndex = null,
	onFocusCardIndexChange,
} ) {
	// Collapsed until the user toggles; survives Fields ↔ Customization switches.
	const [ elementsOpen, setElementsOpen ] = useState( false );
	const hasOrder =
		showOrderTab &&
		( meta.fieldSchema || [] ).some(
			( field ) => field?.type === 'elementsOrder'
		);
	const tabs = [
		{
			name: 'fields',
			title: createElement(
				Fragment,
				null,
				createElement( 'span', {
					className: 'dashicons dashicons-edit',
					'aria-hidden': true,
				} ),
				createElement(
					'span',
					null,
					__( 'Fields', 'joc-blockora-morph' )
				)
			),
		},
		{
			name: 'styles',
			title: createElement(
				Fragment,
				null,
				createElement( 'span', {
					className: 'dashicons dashicons-art',
					'aria-hidden': true,
				} ),
				createElement(
					'span',
					null,
					__( 'Customize', 'joc-blockora-morph' )
				)
			),
		},
	];
	if ( hasOrder ) {
		tabs.push( {
			name: 'order',
			title: createElement(
				Fragment,
				null,
				createElement( 'span', {
					className: 'dashicons dashicons-sort',
					'aria-hidden': true,
				} ),
				createElement(
					'span',
					null,
					__( 'Order', 'joc-blockora-morph' )
				)
			),
		} );
	}

	const panelClass =
		className + ( hasOrder ? ' bm-gutenberg-tabs--with-order' : '' );

	const instanceLocked =
		enableStyleScopeGate &&
		! isIndependentInstanceScope( attributes?.styleScopeChoices );

	const unlockInstance = () => {
		onEnsureSidebar?.();
		const surfaceId =
			styleScopeSurfaceId || ( meta?.id ? `ub-${ meta.id }` : 'anon' );
		rememberStyleScope( surfaceId, BLOCK_STYLE_SCOPE_KEY, 'instance' );
		setAttributes(
			buildIndependentInstancePatch(
				attributes,
				libraryCssVars != null
					? normalize( libraryCssVars )
					: normalize( meta.cssVars || {} ),
				meta
			)
		);
	};

	const lockedGate = instanceLocked
		? createElement( StyleScopeGate, {
				onCustomizeInstance: unlockInstance,
				editMyBlockUrl: myBlockEditorUrl( meta?.id ),
		  } )
		: null;

	return createElement(
		TabPanel,
		{
			key: `bm-tabs-${ tabPanelKey }`,
			className: panelClass,
			activeClass: 'is-active',
			initialTabName,
			onSelect: ( name ) => onTabSelect?.( name ),
			tabs,
		},
		( tab ) => {
			if ( lockedGate ) {
				return lockedGate;
			}
			if ( tab.name === 'fields' ) {
				return createElement( FieldControls, {
					meta,
					attributes,
					setAttributes,
					onEditButtonStyles,
					renderRichText,
					breakpoint: styleBreakpoint,
					enableStyleScopeGate,
					onFocusCardIndex: onFocusCardIndexChange,
					allowManageButtonItems: true,
				} );
			}
			if ( tab.name === 'order' ) {
				return createElement( OrderControls, {
					meta,
					attributes,
					setAttributes,
					appliesGlobally: orderAppliesGlobally,
				} );
			}
			return createElement( StyleControls, {
				meta,
				attributes,
				setAttributes,
				selectedElementKey,
				onSelectElement,
				onClearElement,
				focusButtonIndex,
				breakpoint: styleBreakpoint,
				onBreakpointChange: onStyleBreakpointChange,
				onViewSwitch,
				buttonLibrary,
				breakpointHints,
				onEditButtonStyles,
				renderRichText,
				elementsOpen,
				onElementsOpenChange: setElementsOpen,
				enableStyleScopeGate,
				libraryCssVars,
				onEnsureSidebar,
				styleScopeSurfaceId,
				focusSlideIndex,
				onFocusSlideIndexChange,
				focusCardIndex,
				onFocusCardIndexChange,
				allowElementReorder,
			} );
		}
	);
}
