/** Locator result, map, and composition widgets. */
import { useEffect, useRef, useState, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { DesignView, resultsContainerClass, buildDesignCss, nodeClass, useSharedDesignStyles, WithIcon } from '@cliqui/design-runtime/designRender';
import SliderControl from '@cliqui/design-runtime/SliderControl';
import { useLocator } from './context';
import { normaliseRadius } from './searchState.mjs';
import { modelOf } from './designModels.mjs';
import { SearchForm, GlideSelect } from './searchWidgets';
import { MapWidget } from './widgets/MapWidget';

export const SearchWidget = ( { ws } ) => <SearchForm ws={ ws } />;

// ---- Distance -------------------------------------------------------------
export const DistanceWidget = ( { ws } ) => {
	const { query, setQuery } = useLocator();
	const unit = ws.unit || 'km';
	const min = ws.min ?? 5;
	const max = ws.max ?? 200;
	const step = ws.step ?? 5;
	const set = ( v ) => setQuery( ( q ) => ( { ...q, radius: normaliseRadius( v, q.radius ), unit } ) );
	const radius = normaliseRadius( query.radius, ws.default ?? min );

	return (
		<div className="cliq-sl-w cliq-sl-w--distance cliq-sl-distance">
			{ ws.label ? <label className="cliq-sl-w__label">{ ws.label }</label> : null }
			{ ws.control === 'slider' ? (
				<div className="cliq-sl-distance__slider">
					<SliderControl min={ min } max={ max } step={ step } value={ radius } onChange={ ( v ) => set( v ) } />
					<span>{ radius } { unit }</span>
				</div>
			) : (
				<GlideSelect
					options={ Array.from( { length: Math.floor( ( max - min ) / step ) + 1 }, ( _, i ) => min + i * step ).map( ( r ) => ( { value: String( r ), label: `${ r } ${ unit }` } ) ) }
					value={ String( radius ) }
					onChange={ ( v ) => set( v ) }
				/>
			) }
		</div>
	);
};

// ---- Category / Brand filter ----------------------------------------------
export const CategoryWidget = ( { ws } ) => {
	const { query, setQuery, config, bootstrapData, designs, categoryCounts } = useLocator();
	// SSR-seeded categories skip the fetch (they're the same /categories payload).
	const [ cats, setCats ] = useState( () => bootstrapData?.categories || [] );
	useEffect( () => {
		if ( bootstrapData?.categories ) return undefined;
		let live = true;
		fetch( `${ config.restUrl }/categories${ config.lang ? `?lang=${ config.lang }` : '' }` )
			.then( ( r ) => r.json() )
			.then( ( res ) => live && setCats( res?.categories || ( Array.isArray( res ) ? res : [] ) ) )
			.catch( () => {} );
		return () => { live = false; };
	}, [ config.restUrl ] );

	const pick = ( v ) => setQuery( ( q ) => ( { ...q, category: v } ) );
	const options = cats.map( ( c ) => ( { value: String( c.id ), label: c.name } ) );
	// Resolved unconditionally: the tiles branch below is conditional, and hooks
	// must run on every render. One stylesheet serves every tile.
	const tileModel = useMemo( () => modelOf( designs, ws.categoryTileDesignId ), [ designs, ws.categoryTileDesignId ] );
	const { scope: tileScope, css: tileCss } = useSharedDesignStyles( tileModel, { tokens: config?.categoryTokens || [], breakpoint: config?.mobileBreakpoint } );

	// Visual tiles that FILTER a separate results widget (unlike the Results
	// widget's own categories mode, which replaces itself with the store list).
	// Flat + single-select: click sets the category filter, click again clears
	// it. Counts come from the search facet (live per radius/filters), and a
	// category with none in the current search drops out.
	if ( ws.display === 'tiles' ) {
		const catModel = tileModel;
		const facet = categoryCounts || null;
		const countOf = ( c ) => ( facet ? ( facet[ c.id ] || 0 ) : ( c.count || 0 ) );
		const shown = cats.filter( ( c ) => ! facet || countOf( c ) > 0 );
		const autoColorCss = shown
			.filter( ( c ) => c.color )
			.map( ( c ) => `.cliq-sl-cat-c${ parseInt( c.id ) || 0 }{--cliq-auto-color:${ String( c.color ).replace( /[\\;{}<>]/g, '' ) }}` )
			.join( '\n' );
		if ( ! catModel ) {
			return <div className="cliq-sl-w cliq-sl-w--category cliq-sl-results--empty">{ __( 'No category tile design selected.', 'cliq-store-locator' ) }</div>;
		}
		return (
			<div className="cliq-sl-w cliq-sl-w--category cliq-sl-cattiles">
				{ ws.label ? <span className="cliq-sl-w__label">{ ws.label }</span> : null }
				{ autoColorCss ? <style>{ autoColorCss }</style> : null }
				{ tileCss ? <style>{ tileCss }</style> : null }
				<div className="cliq-sl-catgrid">
					{ shown.map( ( c ) => {
						const active = String( query.category ) === String( c.id );
						return (
							<button
								type="button"
								key={ c.id }
								aria-pressed={ active }
								className={ `cliq-sl-cat cliq-sl-cat-c${ parseInt( c.id ) || 0 }${ active ? ' is-active' : '' }` }
								onClick={ () => pick( active ? '' : c.id ) }
							>
								<DesignView scope={ tileScope } model={ catModel } record={ { ...c, count: countOf( c ) } } tokens={ config?.categoryTokens || [] } breakpoint={ config?.mobileBreakpoint } />
							</button>
						);
					} ) }
				</div>
			</div>
		);
	}

	if ( ws.display === 'dropdown' ) {
		// Multi-select stores an array of ids; the search endpoint OR-matches any of
		// them (see StoreReadRepository::storeIdsForPivot()). Single-select (default)
		// keeps the original scalar id, unchanged for anyone not opting in.
		return (
			<div className="cliq-sl-w cliq-sl-w--category cliq-sl-filter">
				{ ws.label ? <label className="cliq-sl-w__label">{ ws.label }</label> : null }
				<GlideSelect
					options={ options }
					value={ ws.multiSelect ? ( Array.isArray( query.category ) ? query.category : [] ) : ( query.category || '' ) }
					onChange={ ( v ) => pick( ws.multiSelect ? ( v || [] ) : ( v || '' ) ) }
					placeholder={ ws.allLabel || __( 'All Categories', 'cliq-store-locator' ) }
					multiple={ !! ws.multiSelect }
					searchable={ options.length > 8 }
					fuzzy={ options.length > 8 }
					clearable
				/>
			</div>
		);
	}
	return (
		<div className="cliq-sl-w cliq-sl-w--category cliq-sl-chips">
			<button type="button" className={ `cliq-sl-chip${ ! query.category ? ' is-active' : '' }` } onClick={ () => pick( '' ) }>{ ws.allLabel || __( 'All Categories', 'cliq-store-locator' ) }</button>
			{ cats.map( ( c ) => (
				<button type="button" key={ c.id } className={ `cliq-sl-chip${ String( query.category ) === String( c.id ) ? ' is-active' : '' }` } onClick={ () => pick( c.id ) }>{ c.name }</button>
			) ) }
		</div>
	);
};

export const BrandWidget = ( { ws } ) => {
	const { query, setQuery, config } = useLocator();
	const [ options, setOptions ] = useState( [] );
	useEffect( () => {
		let live = true;
		fetch( `${ config.restUrl }/field-options?source=brands` )
			.then( ( response ) => response.json() )
			.then( ( response ) => live && setOptions( response?.options || [] ) )
			.catch( () => live && setOptions( [] ) );
		return () => { live = false; };
	}, [ config.restUrl ] );

	return (
		<div className="cliq-sl-w cliq-sl-w--brand cliq-sl-filter">
			{ ws.label ? <label className="cliq-sl-w__label">{ ws.label }</label> : null }
			<GlideSelect
				options={ options }
				value={ query.brand || '' }
				onChange={ ( v ) => setQuery( ( q ) => ( { ...q, brand: v || '' } ) ) }
				placeholder={ ws.allLabel || __( 'All Brands', 'cliq-store-locator' ) }
				clearable
			/>
		</div>
	);
};

// ---- Result count ---------------------------------------------------------
const ResultCountDisplay = ( { label, showPrint, embedded = false } ) => {
	const { total } = useLocator();
	const printResults = async ( event ) => {
		const trigger = event.currentTarget;
		const { printStoreList } = await import( /* webpackChunkName: "cliq-store-print" */ './printStoreList.mjs' );
		printStoreList( trigger );
	};
	return (
		<div className={ embedded ? 'cliq-sl-results-count cliq-sl-count' : 'cliq-sl-w cliq-sl-w--count cliq-sl-count' }>
			<strong>{ label || __( 'Number of shops', 'cliq-store-locator' ) }: { total }</strong>
			{ showPrint ? <button type="button" className="cliq-sl-count__print" onClick={ printResults }>⎙ { __( 'Print', 'cliq-store-locator' ) }</button> : null }
		</div>
	);
};

export const CountWidget = ( { ws } ) => <ResultCountDisplay label={ ws.label } showPrint={ ws.showPrint } />;

// ---- Results: plain flex/grid list ----------------------------------------
const StoreList = ( { ws } ) => {
	const { results, loading, designs, selectedId, setSelectedId, config } = useLocator();
	const tokens = config?.tokens || [];
	const model = useMemo( () => modelOf( designs, ws.listItemDesignId ), [ designs, ws.listItemDesignId ] );
	// Every card in this list shares one stylesheet — the card design's css is
	// the same for all of them. Declared before the early returns below: hooks
	// must run on every render.
	const { scope: cardScope, css: cardCss } = useSharedDesignStyles( model, { tokens, breakpoint: config?.mobileBreakpoint } );

	if ( ! model ) {
		return <div className="cliq-sl-results cliq-sl-results--empty">{ __( 'No result design selected.', 'cliq-store-locator' ) }</div>;
	}
	if ( ! results.length ) {
		// While loading, this is a SUPERSEDED filter's results having just been
		// cleared (fetchPage) — show a loading state, not the permanent empty
		// text, so a category/filter change never flashes stale OR "no results".
		return (
			<div className="cliq-sl-results cliq-sl-results--empty">
				{ loading ? __( 'Loading…', 'cliq-store-locator' ) : ( ws.emptyText || __( 'No results found.', 'cliq-store-locator' ) ) }
			</div>
		);
	}

	return (
		<div className={ resultsContainerClass( ws, 'cliq-sl-results' ) }>
			{ cardCss ? <style>{ cardCss }</style> : null }
			{ results.map( ( store ) => (
				<div
					key={ store.id }
					data-cliq-store-id={ String( store.id ) }
					className={ `cliq-sl-result${ String( selectedId ) === String( store.id ) ? ' is-active' : '' }` }
					onClick={ () => setSelectedId( store.id ) }
					role="button"
					tabIndex={ 0 }
					onKeyDown={ ( e ) => e.key === 'Enter' && setSelectedId( store.id ) }
				>
					<DesignView scope={ cardScope } model={ model } record={ store } tokens={ tokens } breakpoint={ config?.mobileBreakpoint } />
				</div>
			) ) }
		</div>
	);
};

// ---- Results: accordion — ONE card per store that expands in place --------
// The bound design's rows flagged "reveal on expand" form the expandable region;
// the header rows stay visible. Clicking the card smoothly expands it.
const AccordionResults = ( { ws } ) => {
	const { results, loading, designs, selectedId, setSelectedId, config } = useLocator();
	const tokens = config?.tokens || [];
	const model = useMemo( () => modelOf( designs, ws.listItemDesignId ), [ designs, ws.listItemDesignId ] );
	const { scope: cardScope, css: cardCss } = useSharedDesignStyles( model, { tokens, breakpoint: config?.mobileBreakpoint } );
	const single = ws.accordionSingle !== false;
	const [ openIds, setOpenIds ] = useState( [] );

	// Expand whichever store is selected elsewhere (e.g. a marker click).
	useEffect( () => {
		if ( selectedId == null ) return;
		const sid = String( selectedId );
		setOpenIds( ( prev ) => ( prev.includes( sid ) ? prev : ( single ? [ sid ] : [ ...prev, sid ] ) ) );
	}, [ selectedId ] );

	if ( ! model ) {
		return <div className="cliq-sl-results cliq-sl-results--empty">{ __( 'No card design selected.', 'cliq-store-locator' ) }</div>;
	}
	if ( ! results.length ) {
		return (
			<div className="cliq-sl-results cliq-sl-results--empty">
				{ loading ? __( 'Loading…', 'cliq-store-locator' ) : ( ws.emptyText || __( 'No results found.', 'cliq-store-locator' ) ) }
			</div>
		);
	}

	const toggle = ( id ) => {
		const sid = String( id );
		setSelectedId( id );
		setOpenIds( ( prev ) => {
			if ( prev.includes( sid ) ) return prev.filter( ( x ) => x !== sid );
			return single ? [ sid ] : [ ...prev, sid ];
		} );
	};

	// Accordion is always a column list — same container base classes; its
	// gap/scroll values arrive via the generated widget-settings rule
	// (LocatorApp's widgetSettingsCss), inherited from the cell's n-hook.
	return (
		<div className={ resultsContainerClass( { ...ws, layout: 'list', direction: 'column' }, 'cliq-sl-acc-list' ) }>
			{ cardCss ? <style>{ cardCss }</style> : null }
			{ results.map( ( store ) => {
				const open = openIds.includes( String( store.id ) );
				return (
					<div
						key={ store.id }
						data-cliq-store-id={ String( store.id ) }
						className={ `cliq-sl-acc${ open ? ' is-open' : '' }${ String( selectedId ) === String( store.id ) ? ' is-active' : '' }` }
						role="button"
						tabIndex={ 0 }
						aria-expanded={ open }
						onClick={ () => toggle( store.id ) }
						onKeyDown={ ( e ) => ( e.key === 'Enter' || e.key === ' ' ) && ( e.preventDefault(), toggle( store.id ) ) }
					>
						<DesignView scope={ cardScope } model={ model } record={ store } tokens={ tokens } breakpoint={ config?.mobileBreakpoint } accordion={ { open } } />
						<span className="cliq-sl-acc__chev" aria-hidden="true">›</span>
					</div>
				);
			} ) }
		</div>
	);
};

// ---- Results: stores grouped into expandable category/place accordions ----
const groupValueOf = ( store, groupBy ) => {
	if ( groupBy === 'category' ) return store.categories?.[ 0 ]?.name || '';
	if ( groupBy === 'brand' ) return typeof store.brand === 'string' ? store.brand : ( store.brand?.name || '' );
	if ( groupBy === 'city' ) return store.city || store.location?.city || '';
	if ( groupBy === 'state' ) return store.location?.state || '';
	if ( groupBy === 'country' ) return store.country || store.location?.country || '';
	return '';
};

const GroupedAccordionResults = ( { ws } ) => {
	const { results, loading, designs, selectedId, setSelectedId, config } = useLocator();
	const tokens = config?.tokens || [];
	const model = useMemo( () => modelOf( designs, ws.listItemDesignId ), [ designs, ws.listItemDesignId ] );
	const groupBy = ws.groupBy || 'category';
	const { scope: cardScope, css: cardCss } = useSharedDesignStyles( model, { tokens, breakpoint: config?.mobileBreakpoint } );
	const single = ws.groupAccordionSingle !== false;
	const closedIcon = ws.groupClosedIcon === undefined ? 'chevron-right' : ws.groupClosedIcon;
	const openIcon = ws.groupOpenIcon === undefined ? 'chevron-down' : ws.groupOpenIcon;
	const groups = useMemo( () => {
		const grouped = new Map();
		results.forEach( ( store ) => {
			const label = String( groupValueOf( store, groupBy ) || __( 'Other', 'cliq-store-locator' ) );
			if ( ! grouped.has( label ) ) grouped.set( label, [] );
			grouped.get( label ).push( store );
		} );
		return Array.from( grouped, ( [ label, stores ] ) => ( { label, stores } ) )
			.sort( ( a, b ) => a.label.localeCompare( b.label ) );
	}, [ results, groupBy ] );
	const [ openGroups, setOpenGroups ] = useState( [] );

	useEffect( () => {
		if ( selectedId == null ) return;
		const selectedGroup = groups.find( ( group ) => group.stores.some( ( store ) => String( store.id ) === String( selectedId ) ) );
		if ( ! selectedGroup ) return;
		setOpenGroups( ( previous ) => previous.includes( selectedGroup.label )
			? previous
			: ( single ? [ selectedGroup.label ] : [ ...previous, selectedGroup.label ] ) );
	}, [ selectedId, groups, single ] );

	if ( ! model ) {
		return <div className="cliq-sl-results cliq-sl-results--empty">{ __( 'No result design selected.', 'cliq-store-locator' ) }</div>;
	}
	if ( ! results.length ) {
		return (
			<div className="cliq-sl-results cliq-sl-results--empty">
				{ loading ? __( 'Loading…', 'cliq-store-locator' ) : ( ws.emptyText || __( 'No results found.', 'cliq-store-locator' ) ) }
			</div>
		);
	}

	const toggleGroup = ( label ) => setOpenGroups( ( previous ) => {
		if ( previous.includes( label ) ) return previous.filter( ( value ) => value !== label );
		return single ? [ label ] : [ ...previous, label ];
	} );

	return (
		<div className={ resultsContainerClass( { ...ws, layout: 'list', direction: 'column' }, 'cliq-sl-group-list' ) }>
			{ /* Without this the shared scope is applied to every card but its
			     stylesheet is never rendered, so a grouped locator paints the
			     card design completely unstyled. Twin of the list/accordion
			     branches above. */ }
			{ cardCss ? <style>{ cardCss }</style> : null }
			{ groups.map( ( group ) => {
				const open = openGroups.includes( group.label );
				const toggleIcon = open ? openIcon : closedIcon;
				return (
					<section className={ `cliq-sl-group${ open ? ' is-open' : '' }` } key={ group.label }>
						<button type="button" className="cliq-sl-group__toggle" aria-expanded={ open } onClick={ () => toggleGroup( group.label ) }>
							<span>{ group.label }</span>
							{ ws.groupShowCount !== false && <span className="cliq-sl-group__count">{ group.stores.length }</span> }
							{ toggleIcon && <span className="cliq-sl-group__chev" aria-hidden="true"><WithIcon item={ { icon: toggleIcon } } /></span> }
						</button>
						<div className="cliq-sl-group__reveal">
							<div className="cliq-sl-group__body">
								{ group.stores.map( ( store ) => (
									<div
										key={ store.id }
										data-cliq-store-id={ String( store.id ) }
										className={ `cliq-sl-result${ String( selectedId ) === String( store.id ) ? ' is-active' : '' }` }
										role="button"
										tabIndex={ 0 }
										onClick={ () => setSelectedId( store.id ) }
										onKeyDown={ ( event ) => event.key === 'Enter' && setSelectedId( store.id ) }
									>
										<DesignView scope={ cardScope } model={ model } record={ store } tokens={ tokens } breakpoint={ config?.mobileBreakpoint } />
									</div>
								) ) }
							</div>
						</div>
					</section>
				);
			} ) }
		</div>
	);
};

// ---- Results: category cards → drill into a category's stores -------------
const CategoryBrowser = ( { ws } ) => {
	const { query, setQuery, config, bootstrapData, designs, categoryCounts } = useLocator();
	// SSR-seeded categories skip the fetch (null = still loading → placeholder).
	const [ cats, setCats ] = useState( () => bootstrapData?.categories || null );
	// Every tile is fully design-driven — no hardcoded circle/name/count markup.
	const catModel = useMemo( () => modelOf( designs, ws.categoryTileDesignId ), [ designs, ws.categoryTileDesignId ] );
	const categoryTokens = config?.categoryTokens || [];
	const { scope: tileScope, css: tileCss } = useSharedDesignStyles( catModel, { tokens: categoryTokens, breakpoint: config?.mobileBreakpoint } );
	// When a search narrows the set, /search returns per-category counts scoped
	// to it (categoryCounts). Use them so a tile reflects "how many in THIS
	// search" and a category with none in view drops out — instead of everything
	// showing its global total. Absent → the category's own global count.
	const facet = categoryCounts || null;
	const countOf = ( c ) => ( facet ? ( facet[ c.id ] || 0 ) : ( c.count || 0 ) );
	const withCount = ( c ) => ( { ...c, count: countOf( c ) } );
	// Hierarchical browsing: which parent's children we're currently viewing
	// (null = top level). Clicking a category WITH children drills in; a leaf
	// category jumps straight to its stores. When a facet is active, hide the
	// branches that have zero stores in the current search.
	const [ drillParent, setDrillParent ] = useState( null );
	const childrenOf = ( pid ) => ( cats || [] ).filter( ( c ) => Number( c.parent_id || 0 ) === Number( pid || 0 ) && ( ! facet || countOf( c ) > 0 ) );
	const hasChildren = ( id ) => childrenOf( id ).length > 0;

	useEffect( () => {
		if ( bootstrapData?.categories ) return undefined;
		let live = true;
		fetch( `${ config.restUrl }/categories${ config.lang ? `?lang=${ config.lang }` : '' }` )
			.then( ( r ) => r.json() )
			.then( ( res ) => live && setCats( res?.categories || ( Array.isArray( res ) ? res : [] ) ) )
			.catch( () => live && setCats( [] ) );
		return () => { live = false; };
	}, [ config.restUrl ] );

	// A category is selected → show its stores with a Back link. Back returns to
	// the selected category's sibling level (its parent's children), not always
	// the top, so hierarchical navigation feels natural.
	if ( query.category ) {
		const current = ( cats || [] ).find( ( c ) => String( c.id ) === String( query.category ) );
		const back = () => {
			// drillParent already holds the level we came from (a parent's
			// children, or top), so just clearing lands us back there.
			setQuery( ( q ) => ( { ...q, category: '' } ) );
		};
		return (
			<div className="cliq-sl-catdrill">
				<button type="button" className="cliq-sl-back" onClick={ back }>
					‹ { __( 'Back to categories', 'cliq-store-locator' ) }{ current ? ` · ${ current.name }` : '' }
				</button>
				<StoreList ws={ ws } />
			</div>
		);
	}

	if ( cats === null ) return <div className="cliq-sl-results cliq-sl-results--empty">{ __( 'Loading…', 'cliq-store-locator' ) }</div>;

	if ( ! catModel ) {
		return <div className="cliq-sl-results cliq-sl-results--empty">{ __( 'No category tile design selected.', 'cliq-store-locator' ) }</div>;
	}

	// Per-category --cliq-auto-color custom property (Colour section's
	// "Automatic" background option), keyed by the SAME stable id class the
	// button below carries — the same generated-stylesheet-per-record pattern
	// DesignView itself uses, never a style attribute. PHP twin:
	// DesignRenderer::autoColorVarCss() (LocatorSsrRenderer).
	const autoColorCss = cats
		.filter( ( c ) => c.color )
		.map( ( c ) => `.cliq-sl-cat-c${ parseInt( c.id ) || 0 }{--cliq-auto-color:${ String( c.color ).replace( /[\\;{}<>]/g, '' ) }}` )
		.join( '\n' );

	// The tiles shown now: children of the drilled-into parent, else top level.
	const parentCat = drillParent ? cats.find( ( c ) => String( c.id ) === String( drillParent ) ) : null;
	const shown = childrenOf( drillParent || 0 );

	return (
		<div className="cliq-sl-catbrowse">
			{ autoColorCss ? <style>{ autoColorCss }</style> : null }
			{ tileCss ? <style>{ tileCss }</style> : null }
			{ parentCat && (
				<button
					type="button"
					className="cliq-sl-back"
					onClick={ () => setDrillParent( Number( parentCat.parent_id || 0 ) > 0 ? Number( parentCat.parent_id ) : null ) }
				>
					‹ { __( 'Back', 'cliq-store-locator' ) } · { parentCat.name }
				</button>
			) }
			<div className="cliq-sl-catgrid">
				{ /* Inside a parent: a "View all" card (same tile design as the
				     others) that searches every store across the whole sub-tree. */ }
				{ parentCat && (
					<button
						type="button"
						className={ `cliq-sl-cat cliq-sl-cat-c${ parseInt( parentCat.id ) || 0 } cliq-sl-cat--viewall` }
						onClick={ () => setQuery( ( q ) => ( { ...q, category: parentCat.id } ) ) }
					>
						<DesignView scope={ tileScope } model={ catModel } record={ { ...withCount( parentCat ), name: __( 'View all', 'cliq-store-locator' ) } } tokens={ categoryTokens } breakpoint={ config?.mobileBreakpoint } />
					</button>
				) }
				{ shown.map( ( c ) => (
					<button
						type="button"
						key={ c.id }
						className={ `cliq-sl-cat cliq-sl-cat-c${ parseInt( c.id ) || 0 }${ hasChildren( c.id ) ? ' cliq-sl-cat--haschildren' : '' }` }
						onClick={ () => ( hasChildren( c.id )
							? setDrillParent( c.id )
							: setQuery( ( q ) => ( { ...q, category: c.id } ) ) ) }
					>
						<DesignView scope={ tileScope } model={ catModel } record={ withCount( c ) } tokens={ categoryTokens } breakpoint={ config?.mobileBreakpoint } />
					</button>
				) ) }
			</div>
		</div>
	);
};

// Infinite-scroll twin of the Load More button: an invisible sentinel that
// fetches the next page once it enters the viewport. Mirrors the button's
// hasMore/loading gating so both modes share the exact same fetch trigger.
const LoadMoreSentinel = ( { loadMore, hasMore, loading } ) => {
	const elRef = useRef( null );
	const stateRef = useRef( { loadMore, hasMore, loading } );
	stateRef.current = { loadMore, hasMore, loading };

	useEffect( () => {
		if ( ! elRef.current || typeof IntersectionObserver === 'undefined' ) return undefined;
		const observer = new IntersectionObserver( ( entries ) => {
			const { hasMore: canLoad, loading: isLoading, loadMore: fetchMore } = stateRef.current;
			if ( entries[ 0 ]?.isIntersecting && canLoad && ! isLoading ) fetchMore();
		} );
		observer.observe( elRef.current );
		return () => observer.disconnect();
	}, [] );

	return hasMore ? <div ref={ elRef } className="cliq-sl-loadmore-sentinel" aria-hidden="true" /> : null;
};

// ---- Results widget (dispatches by display mode) --------------------------
export const ResultsWidget = ( { ws } ) => {
	const {
		loadMore, hasMore, loading, refreshing, results, designs, selectedId, setSelectedId,
		config, setMapOnly,
	} = useLocator();
	const mode = ws.displayMode || 'list';
	const showLoadMore = ws.showLoadMore !== false;
	const loadMoreType = ws.loadMoreType || 'button';
	const [ filtersOpen, setFiltersOpen ] = useState( false );
	const selectedStore = results.find( ( store ) => String( store.id ) === String( selectedId ) );
	const detailModel = modelOf( designs, ws.detailDesignId || ws.listItemDesignId );
	const showDetail = !! ( ws.showDetailOnSelect && selectedStore && detailModel );
	const mapOnlyLabel = ws.mapToggleInactiveLabel === undefined
		? __( 'Show map only', 'cliq-store-locator' )
		: ws.mapToggleInactiveLabel;

	if ( showDetail ) {
		return (
			<div className="cliq-sl-w cliq-sl-w--results cliq-sl-selected-detail">
				<button type="button" className="cliq-sl-back" onClick={ () => setSelectedId( null ) }>← { ws.detailBackLabel || __( 'Back to list', 'cliq-store-locator' ) }</button>
				<DesignView model={ detailModel } record={ selectedStore } tokens={ config?.tokens || [] } breakpoint={ config?.mobileBreakpoint } />
			</div>
		);
	}

	return (
		<div className={ `cliq-sl-w cliq-sl-w--results${ refreshing && results.length ? ' is-updating' : '' }` } aria-busy={ refreshing ? 'true' : undefined }>
			{ refreshing && results.length ? <span className="cliq-sl-results-progress" aria-hidden="true" /> : null }
			{ ws.showResultCount ? <ResultCountDisplay label={ ws.resultCountLabel } showPrint={ ws.resultCountShowPrint } embedded /> : null }
			{ ( ws.showFilterDrawer || ws.allowMapOnly ) && (
				<div className="cliq-sl-results-tools">
					{ ws.showFilterDrawer && (
						<button type="button" className={ `cliq-sl-results-tools__button${ filtersOpen ? ' is-active' : '' }` } aria-expanded={ filtersOpen } onClick={ () => setFiltersOpen( ( open ) => ! open ) }>
							{ /* Was a hardcoded ☷ glyph, which rendered as a box in most
							     fonts and could not be themed or changed. */ }
							<WithIcon item={ { icon: ws.filterButtonIcon || 'sliders-horizontal', iconPos: 'left' } }>
								{ ws.filterButtonLabel || __( 'Filters', 'cliq-store-locator' ) }
							</WithIcon>
						</button>
					) }
					{ ws.allowMapOnly && (
						<button type="button" className="cliq-sl-results-tools__button cliq-sl-map-view-toggle" title={ mapOnlyLabel || __( 'Show map only', 'cliq-store-locator' ) } aria-label={ mapOnlyLabel || __( 'Show map only', 'cliq-store-locator' ) } onClick={ () => setMapOnly( true ) }>
							<WithIcon item={ { icon: ws.mapToggleInactiveIcon || 'arrow-right', iconPos: 'right' } }>{ mapOnlyLabel }</WithIcon>
						</button>
					) }
				</div>
			) }
			{ filtersOpen ? (
				<div className="cliq-sl-filter-drawer">
					<div className="cliq-sl-filter-drawer__header">
						<span aria-hidden="true" />
						<strong>{ __( 'Filters', 'cliq-store-locator' ) }</strong>
						<button type="button" aria-label={ __( 'Close filters', 'cliq-store-locator' ) } onClick={ () => setFiltersOpen( false ) }>×</button>
					</div>
					{ ws.filterSearchFormId
						? <SearchForm ws={ { searchFormId: ws.filterSearchFormId } } />
						: <div className="cliq-sl-results--empty">{ __( 'Choose a Search Form for this filter drawer.', 'cliq-store-locator' ) }</div> }
				</div>
			) : (
				<>
					{ mode === 'accordion' ? <AccordionResults ws={ ws } />
						: mode === 'grouped' ? <GroupedAccordionResults ws={ ws } />
						: mode === 'categories' ? <CategoryBrowser ws={ ws } />
							: <StoreList ws={ ws } /> }
			{ /* Pagination: fetches the next page and APPENDS — the list grows
			     and the map accumulates the new stores' markers. Hidden once
			     everything is loaded (or when the widget is set to load all
			     results at once — hasMore is then already false). */ }
			{ showLoadMore && hasMore && (
				loadMoreType === 'lazy'
					? <LoadMoreSentinel loadMore={ loadMore } hasMore={ hasMore } loading={ loading } />
					: (
						<button type="button" className="cliq-sl-loadmore" disabled={ loading } onClick={ loadMore }>
							<WithIcon item={ { icon: ws.loadMoreIcon, iconPos: ws.loadMoreIconPosition || 'right' } }>
								{ loading ? __( 'Loading…', 'cliq-store-locator' ) : ( ws.loadMoreLabel || __( 'Load more', 'cliq-store-locator' ) ) }
							</WithIcon>
						</button>
					)
			) }
				</>
			) }
		</div>
	);
};

// ---- Map ------------------------------------------------------------------
export const WIDGETS = {
	search: SearchWidget,
	distance: DistanceWidget,
	category: CategoryWidget,
	brand: BrandWidget,
	count: CountWidget,
	results: ResultsWidget,
	map: MapWidget,
};
