/**
 * Responsive element stack order (desktop / tablet / mobile).
 * Legacy flat string[] is treated as desktop-only.
 * Front-end order uses CSS `order` so one DOM works across breakpoints.
 */

export const BREAKPOINTS = [ 'desktop', 'tablet', 'mobile' ];

const MEDIA = {
	tablet: '(max-width: 1024px)',
	mobile: '(max-width: 480px)',
};

export const DEFAULT_ELEMENTS_ORDER = [
	'badge',
	'header',
	'subheader',
	'image',
	'logos',
	'buttons',
];

/**
 * Prefer global default stack, then any extra partial keys.
 *
 * @param {string[]} partialKeys
 * @return {string[]}
 */
export function defaultsForPartials( partialKeys = [] ) {
	const available = ( Array.isArray( partialKeys ) ? partialKeys : [] ).filter(
		( k ) => typeof k === 'string' && k
	);
	if ( ! available.length ) {
		return [ ...DEFAULT_ELEMENTS_ORDER ];
	}
	const ordered = DEFAULT_ELEMENTS_ORDER.filter( ( k ) =>
		available.includes( k )
	);
	available.forEach( ( key ) => {
		if ( ! ordered.includes( key ) ) {
			ordered.push( key );
		}
	} );
	return ordered;
}

/**
 * @param {unknown} value
 * @param {string[]} defaults
 * @return {string[]}
 */
export function resolveOrderList( value, defaults = DEFAULT_ELEMENTS_ORDER ) {
	const base = Array.isArray( defaults ) && defaults.length
		? defaults.filter( ( k ) => typeof k === 'string' && k )
		: [ ...DEFAULT_ELEMENTS_ORDER ];
	const next = [];
	if ( Array.isArray( value ) && value.length ) {
		value.forEach( ( key ) => {
			if ( typeof key !== 'string' || ! key || next.includes( key ) ) {
				return;
			}
			// Keep authoring order, including card keys not in hero defaults.
			next.push( key );
		} );
	}
	if ( ! next.length ) {
		return collapseOrderGroups( [ ...base ] );
	}
	base.forEach( ( key ) => {
		if ( ! next.includes( key ) ) {
			next.push( key );
		}
	} );
	return collapseOrderGroups( next );
}

/**
 * @param {unknown} value
 * @return {boolean}
 */
function isOrderMap( value ) {
	return (
		value &&
		typeof value === 'object' &&
		! Array.isArray( value ) &&
		BREAKPOINTS.some( ( bp ) => Object.prototype.hasOwnProperty.call( value, bp ) )
	);
}

/**
 * Normalize to { desktop: string[], tablet: string[]|null, mobile: string[]|null }.
 * null = inherit from parent breakpoint.
 *
 * @param {unknown} value
 * @param {string[]} [defaults]
 * @return {{ desktop: string[], tablet: string[]|null, mobile: string[]|null }}
 */
export function normalizeElementsOrder( value, defaults = DEFAULT_ELEMENTS_ORDER ) {
	if ( Array.isArray( value ) ) {
		return {
			desktop: resolveOrderList( value, defaults ),
			tablet: null,
			mobile: null,
		};
	}
	if ( isOrderMap( value ) ) {
		const desktop = resolveOrderList( value.desktop, defaults );
		const tabletRaw = value.tablet;
		const mobileRaw = value.mobile;
		return {
			desktop,
			tablet:
				Array.isArray( tabletRaw ) && tabletRaw.length
					? resolveOrderList( tabletRaw, defaults )
					: null,
			mobile:
				Array.isArray( mobileRaw ) && mobileRaw.length
					? resolveOrderList( mobileRaw, defaults )
					: null,
		};
	}
	return {
		desktop: resolveOrderList( null, defaults ),
		tablet: null,
		mobile: null,
	};
}

/**
 * Effective flat order for a breakpoint (desktop → tablet → mobile cascade).
 *
 * @param {unknown} value
 * @param {string}  [breakpoint]
 * @param {string[]} [defaults]
 * @return {string[]}
 */
export function effectiveElementsOrder(
	value,
	breakpoint = 'desktop',
	defaults = DEFAULT_ELEMENTS_ORDER
) {
	const n = normalizeElementsOrder( value, defaults );
	let out = n.desktop;
	if ( breakpoint === 'tablet' || breakpoint === 'mobile' ) {
		if ( n.tablet ) {
			out = n.tablet;
		}
	}
	if ( breakpoint === 'mobile' && n.mobile ) {
		out = n.mobile;
	}
	return out;
}

/**
 * @param {string[]} a
 * @param {string[]} b
 * @return {boolean}
 */
function sameOrder( a, b ) {
	if ( ! Array.isArray( a ) || ! Array.isArray( b ) || a.length !== b.length ) {
		return false;
	}
	return a.every( ( key, i ) => key === b[ i ] );
}

function isRedundantDefaultOrderValue( value, defaults = DEFAULT_ELEMENTS_ORDER ) {
	const n = normalizeElementsOrder( value, defaults );
	if ( Array.isArray( n.tablet ) || Array.isArray( n.mobile ) ) {
		return false;
	}
	return sameOrder( n.desktop, resolveOrderList( defaults, DEFAULT_ELEMENTS_ORDER ) );
}

/**
 * Store order at a breakpoint; tablet/mobile stay sparse when equal to parent.
 *
 * @param {unknown}  value
 * @param {string}   breakpoint
 * @param {string[]} nextList
 * @param {string[]} [defaults]
 * @return {{ desktop: string[], tablet: string[]|null, mobile: string[]|null }}
 */
export function setElementsOrderAtBreakpoint(
	value,
	breakpoint,
	nextList,
	defaults = DEFAULT_ELEMENTS_ORDER
) {
	const bp = BREAKPOINTS.includes( breakpoint ) ? breakpoint : 'desktop';
	const n = normalizeElementsOrder( value, defaults );
	const next = resolveOrderList( nextList, defaults );

	if ( bp === 'desktop' ) {
		const out = { ...n, desktop: next };
		if ( out.tablet && sameOrder( out.tablet, next ) ) {
			out.tablet = null;
		}
		const tabletEff = out.tablet || next;
		if ( out.mobile && sameOrder( out.mobile, tabletEff ) ) {
			out.mobile = null;
		}
		return out;
	}

	if ( bp === 'tablet' ) {
		const out = { ...n };
		out.tablet = sameOrder( next, out.desktop ) ? null : next;
		const tabletEff = out.tablet || out.desktop;
		if ( out.mobile && sameOrder( out.mobile, tabletEff ) ) {
			out.mobile = null;
		}
		return out;
	}

	const tabletEff = n.tablet || n.desktop;
	return {
		...n,
		mobile: sameOrder( next, tabletEff ) ? null : next,
	};
}

/**
 * Persist shape for attributes (omit null breakpoints).
 *
 * @param {unknown} value
 * @param {string[]} [defaults]
 * @return {{ desktop: string[], tablet?: string[], mobile?: string[] }}
 */
export function serializeElementsOrder( value, defaults = DEFAULT_ELEMENTS_ORDER ) {
	const n = normalizeElementsOrder( value, defaults );
	const out = { desktop: n.desktop };
	if ( n.tablet ) {
		out.tablet = n.tablet;
	}
	if ( n.mobile ) {
		out.mobile = n.mobile;
	}
	return out;
}

/**
 * @param {string} key
 * @return {string}
 */
function sanitizeElKey( key ) {
	return String( key || '' ).replace( /[^a-zA-Z0-9_-]/g, '' );
}

/**
 * Testimonial avatar/name/company live inside `.bm-testimonial__meta`. CSS
 * `order` on those nested nodes breaks the footer row and leaves the footer
 * itself at order 0 (above quote/content). Collapse them to a single `meta`
 * stack key (safe: only when name/company/meta appear — not blog card images).
 *
 * @param {string[]} order
 * @return {string[]}
 */
export function collapseOrderGroups( order ) {
	const list = ( Array.isArray( order ) ? order : [] )
		.map( ( key ) => sanitizeElKey( key ) )
		.filter( Boolean );
	const needsMeta = list.some(
		( key ) => key === 'meta' || key === 'name' || key === 'company'
	);
	if ( ! needsMeta ) {
		return list;
	}
	const out = [];
	let seenMeta = false;
	list.forEach( ( key ) => {
		if (
			key === 'meta' ||
			key === 'image' ||
			key === 'name' ||
			key === 'company'
		) {
			if ( ! seenMeta ) {
				out.push( 'meta' );
				seenMeta = true;
			}
			return;
		}
		out.push( key );
	} );
	return out;
}

/**
 * CSS rules for one flat order list.
 *
 * @param {string}   selector
 * @param {string[]} order
 * @return {string}
 */
function orderRules( selector, order ) {
	const parts = [];
	const collapsed = collapseOrderGroups( order );
	collapsed.forEach( ( key, index ) => {
		const safe = sanitizeElKey( key );
		if ( ! safe ) {
			return;
		}
		parts.push(
			`${ selector } [data-bm-el="${ safe }"]{order:${ index + 1 }}`
		);
	} );
	if ( collapsed.includes( 'meta' ) ) {
		parts.push(
			`${ selector } .bm-testimonial__meta [data-bm-el]{order:unset}`
		);
		// Image alignment (left/right/top/bottom) uses order on the avatar
		// flex item — including the editor hit-wrap around the img.
		parts.push(
			`${ selector } .bm-testimonial__meta > .bm-testimonial__avatar,` +
				`${ selector } .bm-testimonial__meta > .bm-el-hit,` +
				`${ selector } .bm-testimonial__meta > .bm-media-frame` +
				`{order:var(--bm-author-image-order, 0)}`
		);
	}
	return parts.join( '' );
}

/**
 * Full responsive CSS for element stack order.
 *
 * @param {string}   selector
 * @param {unknown}  value
 * @param {string[]} [defaults]
 * @return {string}
 */
export function toElementsOrderCss(
	selector,
	value,
	defaults = DEFAULT_ELEMENTS_ORDER
) {
	if ( ! selector ) {
		return '';
	}
	// When no explicit reordering was requested, DOM insertion order already
	// matches the default stack order; skip emitting `order:` CSS.
	if ( value == null || ( Array.isArray( value ) && value.length === 0 ) ) {
		return '';
	}
	if ( isRedundantDefaultOrderValue( value, defaults ) ) {
		return '';
	}
	const n = normalizeElementsOrder( value, defaults );
	const parts = [ orderRules( selector, n.desktop ) ];
	const tablet = effectiveElementsOrder( n, 'tablet', defaults );
	const mobile = effectiveElementsOrder( n, 'mobile', defaults );
	if ( ! sameOrder( tablet, n.desktop ) ) {
		parts.push(
			`@media ${ MEDIA.tablet }{${ orderRules( selector, tablet ) }}`
		);
	}
	if ( ! sameOrder( mobile, tablet ) ) {
		parts.push(
			`@media ${ MEDIA.mobile }{${ orderRules( selector, mobile ) }}`
		);
	}
	return parts.filter( Boolean ).join( '\n' );
}

/**
 * Bake order CSS for a single editor preview breakpoint.
 *
 * @param {string}   selector
 * @param {unknown}  value
 * @param {string}   [breakpoint]
 * @param {string[]} [defaults]
 * @return {string}
 */
export function toElementsOrderCssForBreakpoint(
	selector,
	value,
	breakpoint = 'desktop',
	defaults = DEFAULT_ELEMENTS_ORDER
) {
	if ( ! selector ) {
		return '';
	}
	return orderRules(
		selector,
		effectiveElementsOrder( value, breakpoint, defaults )
	);
}

/**
 * Defaults from field schema elementsOrder field.
 *
 * @param {Array} [fieldSchema]
 * @param {object} [defaultAttrs]
 * @return {string[]}
 */
export function elementsOrderDefaultsFromSchema(
	fieldSchema = [],
	defaultAttrs = {}
) {
	const orderField = ( fieldSchema || [] ).find(
		( f ) => f?.type === 'elementsOrder'
	);
	if ( Array.isArray( orderField?.default ) && orderField.default.length ) {
		return orderField.default.filter( ( k ) => typeof k === 'string' && k );
	}
	const fromAttrs = defaultAttrs?.elementsOrder;
	if ( Array.isArray( fromAttrs ) && fromAttrs.length ) {
		return fromAttrs.filter( ( k ) => typeof k === 'string' && k );
	}
	if ( fromAttrs && typeof fromAttrs === 'object' && Array.isArray( fromAttrs.desktop ) ) {
		return fromAttrs.desktop.filter( ( k ) => typeof k === 'string' && k );
	}
	// Cards / non-hero blocks: preserve styleable field order from schema
	// instead of the hero DEFAULT_ELEMENTS_ORDER (badge, header, …).
	const styleableKeys = ( fieldSchema || [] )
		.filter( ( f ) => f?.elementStyle === true && f?.key )
		.map( ( f ) => String( f.key ) );
	if ( styleableKeys.length ) {
		return styleableKeys;
	}
	return [ ...DEFAULT_ELEMENTS_ORDER ];
}

/**
 * CSS for block-level + per-slide responsive orders.
 *
 * @param {string} blockSelector
 * @param {object} attrs
 * @param {string[]} [defaults]
 * @param {string|null} [breakpoint] When set, bake one BP (editor preview)
 * @return {string}
 */
export function toElementsOrderCssForAttrs(
	blockSelector,
	attrs = {},
	defaults = DEFAULT_ELEMENTS_ORDER,
	breakpoint = null
) {
	if ( ! blockSelector ) {
		return '';
	}
	const bake =
		breakpoint && BREAKPOINTS.includes( breakpoint )
			? ( sel, value, defs ) =>
					toElementsOrderCssForBreakpoint( sel, value, breakpoint, defs )
			: ( sel, value, defs ) => toElementsOrderCss( sel, value, defs );

	const parts = [];
	const blockOrder = attrs?.elementsOrder;
	const blockCss = bake( blockSelector, blockOrder, defaults );
	if ( blockCss ) {
		parts.push( blockCss );
	}

	const slides = Array.isArray( attrs?.slides ) ? attrs.slides : [];
	slides.forEach( ( slide, index ) => {
		if ( ! slide || typeof slide !== 'object' ) {
			return;
		}
		const sel = `${ blockSelector } [data-bm-slide-index="${ Number( index ) }"]`;
		const slideCss = bake( sel, slide.elementsOrder, defaults );
		if ( slideCss ) {
			parts.push( slideCss );
		}
	} );

	return parts.filter( Boolean ).join( '\n\n' );
}
