/** Map widget and its map-owned navigation/directions workflow. */
import { useCallback, useEffect, useRef, useState, useMemo, createElement, createPortal, render } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { DesignView, buildDesignCss, nodeClass, WithIcon } from '@cliqui/design-runtime/designRender';
import { useLocator } from '../context';
import { createProvider, isProviderReady } from '@cliqui/design-runtime/map/createProvider';
import { shouldOverlayDirectionsOnResults } from '../../../shared/directionsPlacement.mjs';
import { resolveDirectionsRequest } from '../directionsRequest.mjs';
import { isTrustedDirectionsOrigin, resolveAddressOrigin } from '../directionsOrigin.mjs';
import { scheduleStoreResultScroll } from '../resultSelection.mjs';
import { modelOf } from '../designModels.mjs';
import { geocodeAddress } from '../geocode.mjs';
import { SearchForm, LocationAutocompleteInput } from '../searchWidgets';

export const MapWidget = ( { ws } ) => {
	const {
		results, designs, selectedId, setSelectedId, config, query, mapOnly,
		locationPromptOpen, locating, requestLocation, setLocationPromptOpen,
		locationPromptSettings, setGeoError,
	} = useLocator();
	const provider = config?.mapProvider === 'google' ? 'google' : 'leaflet';
	const wrapRef = useRef( null );
	const elRef = useRef( null );
	const providerRef = useRef( null );
	const panelRef = useRef( null ); // turn-by-turn steps container (provider renders here)
	const closeButtonRef = useRef( null );
	const directionTriggerRef = useRef( null );
	const directionRequestRef = useRef( 0 );
	const queryRef = useRef( query );
	queryRef.current = query;
	const [ ready, setReady ] = useState( false );
	const [ dir, setDir ] = useState( null ); // active route summary { origin, distance, duration }
	const [ originPrompt, setOriginPrompt ] = useState( null );
	// A route origin belongs to the directions workflow, not the Store Locator
	// search query. Keeping it local prevents an entered "From address" from
	// filtering, sorting or changing the Search Form's location field.
	const [ directionsOrigin, setDirectionsOrigin ] = useState( null );
	const [ isFullscreen, setIsFullscreen ] = useState( false );
	const [ directionsTarget, setDirectionsTarget ] = useState( null );
	const [ directionsHost ] = useState( () => {
		if ( typeof document === 'undefined' ) return null;
		const host = document.createElement( 'div' );
		host.className = 'cliq-sl-directions-host';
		return host;
	} );
	const directionsInResults = shouldOverlayDirectionsOnResults( {
		placement: ws.directionsPlacement || 'map',
		hasResultsTarget: !! directionsTarget,
		mapOnly,
		isFullscreen,
	} );
	const markerOverride = ( ws.markerType || 'icon' ) === 'icon' && ws.overrideMarkers
		? config?.markers?.[ String( ws.markerOverrideId || '' ) ] || null
		: null;
	const navigableResults = results.filter( ( store ) => {
		// Public REST results keep coordinates on the formatted primary location
		// as `latitude` / `longitude`. Retain the shorter aliases for integrations
		// that supply their own result records.
		const lat = store?.lat ?? store?.latitude ?? store?.location?.lat ?? store?.location?.latitude;
		const lng = store?.lng ?? store?.longitude ?? store?.location?.lng ?? store?.location?.longitude;
		return Number.isFinite( parseFloat( lat ) ) && Number.isFinite( parseFloat( lng ) );
	} );
	const selectMapStore = useCallback( ( storeId ) => {
		setSelectedId( storeId );
		scheduleStoreResultScroll( wrapRef.current?.closest( '.cliq-sl' ), storeId, window );
	}, [ setSelectedId ] );
	const navigateMarker = ( direction ) => {
		if ( ! navigableResults.length ) return;
		const current = navigableResults.findIndex( ( store ) => String( store.id ) === String( selectedId ) );
		const nextIndex = current < 0
			? ( direction > 0 ? 0 : navigableResults.length - 1 )
			: ( current + direction + navigableResults.length ) % navigableResults.length;
		const nextId = navigableResults[ nextIndex ].id;
		selectMapStore( nextId );
		// Open synchronously as well as through the selectedId effect. This makes
		// repeated navigation reliable even when another widget already selected
		// the same store or the state update is batched by Preact.
		providerRef.current?.openStore?.( nextId );
	};
	const markerNavPosition = [ 'sides', 'top-start', 'top-center', 'top-end' ].includes( ws.markerNavigationPosition ) ? ws.markerNavigationPosition : 'sides';
	const toggleFullscreen = () => {
		const wrap = wrapRef.current;
		if ( ! wrap ) return;
		if ( document.fullscreenElement === wrap ) {
			document.exitFullscreen?.();
		} else {
			wrap.requestFullscreen?.();
		}
	};

	useEffect( () => {
		if ( ( ws.directionsPlacement || 'map' ) !== 'results' ) {
			setDirectionsTarget( null );
			return undefined;
		}
		const locator = wrapRef.current?.closest( '.cliq-sl' );
		if ( ! locator ) return undefined;
		const findTarget = () => {
			const target = locator.querySelector( '.cliq-sl-w--results' );
			if ( target ) setDirectionsTarget( target );
			return target;
		};
		if ( findTarget() ) return undefined;
		const observer = new MutationObserver( () => {
			if ( findTarget() ) observer.disconnect();
		} );
		observer.observe( locator, { childList: true, subtree: true } );
		return () => observer.disconnect();
	}, [ ws.directionsPlacement ] );

	useEffect( () => {
		if ( ! directionsTarget ) return undefined;
		const open = !! dir && directionsInResults;
		directionsTarget.classList.toggle( 'is-directions-overlay-open', open );
		return () => directionsTarget.classList.remove( 'is-directions-overlay-open' );
	}, [ directionsTarget, directionsInResults, dir ] );

	// Keep one physical panel node and move only its portal host. Google attaches
	// its DirectionsRenderer to the steps element, so remounting that element when
	// switching between Store List, map-only and fullscreen would detach updates.
	useEffect( () => {
		const parent = directionsInResults ? directionsTarget : wrapRef.current;
		if ( directionsHost && parent && directionsHost.parentNode !== parent ) parent.appendChild( directionsHost );
	}, [ directionsHost, directionsInResults, directionsTarget ] );

	useEffect( () => () => directionsHost?.remove(), [ directionsHost ] );

	useEffect( () => {
		let portalObserver = null;
		const selector = '.glide-dropdown, .pac-container';
		const movePanelsIntoFullscreen = ( root = document.body ) => {
			const wrap = wrapRef.current;
			if ( ! wrap || document.fullscreenElement !== wrap ) return;
			const panels = root.matches?.( selector ) ? [ root ] : [ ...root.querySelectorAll?.( selector ) || [] ];
			panels.forEach( ( panel ) => {
				if ( panel.closest( '.cliq-sl-map-wrap' ) === wrap ) return;
				panel.dataset.cliqFullscreenPortal = '1';
				wrap.appendChild( panel );
			} );
		};
		const restorePanels = () => {
			wrapRef.current?.querySelectorAll( '[data-cliq-fullscreen-portal="1"]' ).forEach( ( panel ) => {
				delete panel.dataset.cliqFullscreenPortal;
				document.body.appendChild( panel );
			} );
		};
		const syncFullscreen = () => {
			const active = document.fullscreenElement === wrapRef.current;
			setIsFullscreen( active );
			portalObserver?.disconnect();
			portalObserver = null;
			if ( active ) {
				movePanelsIntoFullscreen();
				portalObserver = new MutationObserver( ( mutations ) => mutations.forEach( ( mutation ) => mutation.addedNodes.forEach( ( node ) => {
					if ( node.nodeType === 1 ) movePanelsIntoFullscreen( node );
				} ) ) );
				portalObserver.observe( document.body, { childList: true } );
			} else {
				restorePanels();
			}
		};
		document.addEventListener( 'fullscreenchange', syncFullscreen );
		return () => {
			document.removeEventListener( 'fullscreenchange', syncFullscreen );
			portalObserver?.disconnect();
			restorePanels();
		};
	}, [] );

	const startDirections = useCallback( ( origin, destination ) => {
		const activeProvider = providerRef.current;
		if ( ! activeProvider || ! destination ) return Promise.resolve();
		const requestId = ++directionRequestRef.current;
		setGeoError( '' );
		setOriginPrompt( null );
		setDir( { loading: true } );
		return resolveDirectionsRequest(
			() => activeProvider.showDirections( origin, destination, { panel: panelRef.current } ),
			() => requestId === directionRequestRef.current
		).then( ( outcome ) => {
			if ( outcome.state === 'stale' ) return;
			if ( outcome.state === 'unavailable' ) {
				setDir( { error: __( 'A driving route could not be calculated for these locations.', 'cliq-store-locator' ) } );
				return;
			}
			if ( outcome.state === 'failed' ) {
				setDir( { error: __( 'Directions could not be loaded. Please try again.', 'cliq-store-locator' ) } );
				return;
			}
			setDir( outcome.route );
		} );
	}, [ setGeoError ] );

	const closeOriginPrompt = () => {
		setOriginPrompt( null );
		const trigger = directionTriggerRef.current;
		setTimeout( () => trigger?.isConnected && trigger.focus(), 0 );
	};

	useEffect( () => {
		if ( ! originPrompt ) return undefined;
		const closeOnEscape = ( event ) => {
			if ( event.key === 'Escape' && ! originPrompt.locating && ! originPrompt.submitting ) closeOriginPrompt();
		};
		document.addEventListener( 'keydown', closeOnEscape );
		return () => document.removeEventListener( 'keydown', closeOnEscape );
	}, [ originPrompt ] );

	const useCurrentOrigin = () => {
		if ( ! navigator.geolocation ) {
			setOriginPrompt( ( current ) => current ? { ...current, error: __( 'Your browser or connection does not support location access.', 'cliq-store-locator' ) } : current );
			return;
		}
		setOriginPrompt( ( current ) => current ? { ...current, locating: true, error: '' } : current );
		navigator.geolocation.getCurrentPosition(
			( position ) => {
				const origin = { lat: position.coords.latitude, lng: position.coords.longitude, source: 'geolocation' };
				const destination = originPrompt?.destination;
				setDirectionsOrigin( origin );
				startDirections( origin, destination );
			},
			( error ) => {
				const messages = {
					1: __( 'Location access was denied. Enter a starting address below instead.', 'cliq-store-locator' ),
					2: __( 'Your current location could not be determined. Enter a starting address below instead.', 'cliq-store-locator' ),
					3: __( 'Finding your location took too long. Enter a starting address below instead.', 'cliq-store-locator' ),
				};
				setOriginPrompt( ( current ) => current ? { ...current, locating: false, error: messages[ error.code ] || __( 'Could not access your location. Enter a starting address below instead.', 'cliq-store-locator' ) } : current );
			},
			{ timeout: 10000 }
		);
	};

	const submitOriginAddress = async ( event ) => {
		event.preventDefault();
		if ( ! originPrompt || originPrompt.submitting ) return;
		if ( ! originPrompt.selected && ! String( originPrompt.address || '' ).trim() ) {
				setOriginPrompt( ( current ) => current ? { ...current, error: __( 'Enter a starting address or use your current location.', 'cliq-store-locator' ) } : current );
			return;
		}
		setOriginPrompt( ( current ) => current ? { ...current, submitting: true, error: '' } : current );
		const origin = await resolveAddressOrigin(
			originPrompt.selected,
			originPrompt.address,
			( address ) => geocodeAddress( address, config?.restrictCountry || '' )
		);
		if ( ! origin ) {
			setOriginPrompt( ( current ) => current ? { ...current, submitting: false, error: __( 'That starting address could not be found. Choose a suggestion or try a more specific address.', 'cliq-store-locator' ) } : current );
			return;
		}
		setDirectionsOrigin( origin );
		startDirections( origin, originPrompt.destination );
	};

	const closeDirections = () => {
		// A late provider response must not reopen or alter a route the visitor
		// explicitly closed.
		directionRequestRef.current += 1;
		if ( providerRef.current ) providerRef.current.clearDirections();
		if ( panelRef.current ) panelRef.current.innerHTML = '';
		setDir( null );
		const trigger = directionTriggerRef.current;
		setTimeout( () => trigger?.isConnected && trigger.focus(), 0 );
	};

	useEffect( () => {
		if ( dir ) closeButtonRef.current?.focus();
	}, [ dir ] );

	// Map-only/fullscreen changes and the in-map directions panel can change the
	// map's width. Notify the provider during the transition and once at the end.
	useEffect( () => {
		const notifyResize = () => {
			const active = providerRef.current;
			if ( active?.map?.invalidateSize ) {
				active.map.invalidateSize( { pan: false, animate: false } );
			} else if ( active?.g?.maps?.event && active?.map ) {
				active.g.maps.event.trigger( active.map, 'resize' );
			}
		};
		const delays = [ 0, 80, 180, 300, 440 ];
		const timers = delays.map( ( delay ) => setTimeout( notifyResize, delay ) );
		const fitTimer = dir ? setTimeout( () => {
			const active = providerRef.current;
			if ( active?.routeLine?.getBounds && active?.map?.fitBounds ) {
				active.map.fitBounds( active.routeLine.getBounds(), { padding: [ 30, 30 ] } );
			} else {
				const routeBounds = active?.directionsRenderer?.getDirections?.()?.routes?.[ 0 ]?.bounds;
				if ( routeBounds && active?.map?.fitBounds ) active.map.fitBounds( routeBounds );
			}
		}, 460 ) : null;
		return () => {
			timers.forEach( clearTimeout );
			if ( fitTimer ) clearTimeout( fitTimer );
		};
	}, [ mapOnly, dir, directionsInResults, isFullscreen ] );

	// Intercept clicks on a rendered "Directions" button (anywhere in the locator —
	// results card or infobox popup) and draw the route in-house on THIS map, using
	// the searched location (or the visitor's geolocation) as the origin. The
	// external maps link remains a no-JS fallback only. Once this handler sees a
	// panel-mode button, it owns the click and must never also open a new tab.
	useEffect( () => {
		const locator = wrapRef.current?.closest( '.cliq-sl' );
		if ( ! locator ) return undefined;
		const onClick = ( e ) => {
			const btn = e.target.closest && e.target.closest( '[data-cliq-action="directions"]' );
			if ( ! btn ) return;
			// Capture before Google/Leaflet popup handlers. Otherwise an infobox
			// action can also trigger map/popup behaviour during route startup.
			e.preventDefault();
			e.stopPropagation();
			const dlat = parseFloat( btn.getAttribute( 'data-cliq-lat' ) );
			const dlng = parseFloat( btn.getAttribute( 'data-cliq-lng' ) );
			if ( ! Number.isFinite( dlat ) || ! Number.isFinite( dlng ) ) {
				setGeoError( __( 'Directions are unavailable because this store has no map coordinates.', 'cliq-store-locator' ) );
				return;
			}
			if ( ! providerRef.current ) return;
			const dest = { lat: dlat, lng: dlng };
			directionTriggerRef.current = btn;
			const origin = directionsOrigin || queryRef.current?.location;
			if ( directionsOrigin || isTrustedDirectionsOrigin( origin ) ) {
				startDirections( origin, dest );
			} else {
				setGeoError( '' );
				setLocationPromptOpen( false );
				providerRef.current?.clearDirections?.();
				if ( panelRef.current ) panelRef.current.innerHTML = '';
				setDir( null );
				setOriginPrompt( { destination: dest, address: '', selected: null, locating: false, submitting: false, error: '' } );
			}
		};
		locator.addEventListener( 'click', onClick, true );
		return () => locator.removeEventListener( 'click', onClick, true );
	}, [ directionsOrigin, setGeoError, startDirections ] );

	// Render an HTML marker body for a store: the design forced by this widget
	// (ws.markerDesignId, an override) if set, else the store's own resolved
	// marker_design_id (per-store → per-category → global default, all resolved
	// server-side by StorePayloadFormatter). Returns null when no design is
	// available — the provider then falls back to the icon marker for that store.
	const renderMarker = ( store ) => {
		const model = modelOf( designs, ws.markerDesignId || store.marker_design_id );
		if ( ! model ) return null;
		const node = document.createElement( 'div' );
		// The pin anchor (bottom-centre, plus the pointer tail's tip offset when
		// the design has one) is pure stylesheet work: DesignView's generated
		// css puts --cliq-tail-offset on the design root, and style.scss's
		// `.cliq-sl-marker-body > .cliq-widget-design` rule reads it into the
		// anchor transform. No per-node style/property assignments here.
		node.className = 'cliq-sl-marker-body';
		render( createElement( DesignView, { model, record: store, tokens: config?.tokens || [] } ), node );
		return node;
	};

	// Render an infobox popup body for a store using the bound infobox design.
	// Returns a detached DOM node Leaflet can mount; falls back to the store name.
	const renderPopup = ( store ) => {
		const model = modelOf( designs, ws.infoboxDesignId );
		const node = document.createElement( 'div' );
		node.className = 'cliq-sl-popup-body';
		// Expose the infobox design's background + text colour so the provider can
		// colour the popup bubble/tail AND the directions step bubble to match
		// (see GoogleProvider.applyPopupBg).
		const bg = model?.container?.css?.background;
		const fg = model?.container?.css?.color;
		if ( bg ) node.dataset.cliqBg = bg;
		if ( fg ) node.dataset.cliqColor = fg;
		if ( model ) {
			render( createElement( DesignView, { model, record: store, tokens: config?.tokens || [] } ), node );
		} else {
			node.textContent = store.name || '';
		}
		return node;
	};

	// Init the map once the chosen provider's SDK is on the page.
	useEffect( () => {
		let cancelled = false;
		let retryTimer = null;
		const start = async () => {
			if ( cancelled || ! elRef.current || providerRef.current ) return;
			if ( ! isProviderReady( provider ) ) {
				retryTimer = setTimeout( start, 150 );
				return;
			}
			if ( cancelled || ! elRef.current || providerRef.current ) return;
			const instance = await createProvider( provider, elRef.current, {
				// Keep the store-locator css namespace so style.scss matches the
				// adapter's emitted cluster/popup/directions markup.
				classPrefix: 'cliq-sl',
				// 'icon' (today's pins) or 'html' (renders each store's resolved
				// marker design instead) — forces the WHOLE map to one system.
				markerType: ws.markerType || 'icon',
				getMarker: markerOverride ? () => markerOverride : undefined,
				centerLat: ws.centerLat ?? config?.defaultCenter?.lat,
				centerLng: ws.centerLng ?? config?.defaultCenter?.lng,
				zoom: ws.zoom ?? config?.defaultZoom,
				// Zoom applied when a result/marker is clicked, and after a location
				// search — both come from site Settings (clicked_zoom / search_zoom).
				clickedZoom: config?.clickedZoom,
				searchZoom: config?.searchZoom,
				clustering: ws.clustering !== false && config?.clustering !== false,
				// Per-widget override, falling back to the site-wide Settings value.
				clusterColor: ws.clusterColor || config?.clusterColor,
				clusterStyle: ws.clusterStyle || config?.clusterStyle,
				markerAnimation: ws.markerAnimation || config?.markerAnimation,
				// Global marker-size setting (target icon height in px).
				markerSize: config?.markerSize,
				mapType: ws.mapType || config?.mapType,
				// The provider's native control only fullscreens its map canvas and
				// drops our sibling search/navigation UI. The widget owns fullscreen.
				fullscreenControl: false,
				// PHP resolves only the selected global/per-widget Google theme
				// into public bootstrap data. An explicit empty widget array is
				// meaningful: Standard overrides a themed global map.
				styles: Object.prototype.hasOwnProperty.call( ws, 'resolvedMapStyles' )
					? ws.resolvedMapStyles
					: ( config?.resolvedMapStyles || [] ),
				scrollWheel: config?.mouseScroll !== false,
			} );
			if ( cancelled ) {
				instance.destroy?.();
				return;
			}
			if ( instance.init() ) { providerRef.current = instance; setReady( true ); }
		};
		start();
		return () => {
			cancelled = true;
			if ( retryTimer ) clearTimeout( retryTimer );
			if ( providerRef.current ) { providerRef.current.destroy(); providerRef.current = null; }
		};
	}, [] );

	// Sync markers when results (or the bound designs) change.
	useEffect( () => {
		const provider = providerRef.current;
		if ( ! provider || ! ready ) return;
		provider.opts.getMarker = markerOverride ? () => markerOverride : undefined;
		provider.setMarkers( results, {
			onMarkerClick: ( store ) => selectMapStore( store.id ),
			renderPopup,
			renderMarkerHtml: renderMarker,
		} );
	}, [ results, ready, designs, ws.infoboxDesignId, ws.markerType, ws.markerDesignId, ws.overrideMarkers, ws.markerOverrideId, markerOverride, selectMapStore ] );

	// Selecting a store from the list opens its marker popup on the map.
	useEffect( () => {
		const provider = providerRef.current;
		if ( ! provider || ! ready || ! selectedId ) return;
		provider.openStore( selectedId );
	}, [ selectedId, ready ] );

	// Recentre the map on a new location search at the configured search-zoom.
	// Only fires on an actual coordinate change (a search / "my location"), not
	// on every results refresh, so it doesn't fight user panning.
	const prevLocRef = useRef( null );
	useEffect( () => {
		const provider = providerRef.current;
		if ( ! provider || ! ready ) return;
		const loc = query?.location;
		if ( ! loc || loc.lat == null || loc.lng == null ) return;
		const key = `${ loc.lat },${ loc.lng }`;
		if ( prevLocRef.current === key ) return;
		prevLocRef.current = key;
		if ( typeof provider.setCenter === 'function' ) {
			provider.setCenter( loc.lat, loc.lng, config?.searchZoom );
		}
	}, [ query?.location, ready ] );

	// Search-radius circle (Settings → Map → "Draw radius circle"): drawn at the
	// searched location with the active radius, cleared when the search clears.
	useEffect( () => {
		const provider = providerRef.current;
		if ( ! provider || ! ready || ! config?.radiusCircle || typeof provider.drawRadius !== 'function' ) return;
		const loc = query?.location;
		if ( ! loc || loc.lat == null || loc.lng == null ) {
			provider.drawRadius( null, 0 );
			return;
		}
		const meters = ( parseFloat( query?.radius ) || 0 ) * ( ( query?.unit || 'km' ) === 'mi' ? 1609.344 : 1000 );
		if ( meters > 0 ) provider.drawRadius( { lat: parseFloat( loc.lat ), lng: parseFloat( loc.lng ) }, meters );
	}, [ query?.location, query?.radius, query?.unit, ready ] );

	// Height arrives via the generated widget-settings rule
	// (--cliq-sl-map-height on this widget's cell hook, see LocatorApp's
	// widgetSettingsCss) — consumed by .cliq-sl-map-wrap in style.scss.
	const directionsPanel = (
		<aside className={ `cliq-sl-directions${ directionsInResults ? ' cliq-sl-directions--results' : '' }${ dir ? ' is-open' : '' }` } aria-hidden={ ! dir }>
				<div className="cliq-sl-directions__head">
					<strong>{ __( 'Store direction', 'cliq-store-locator' ) }</strong>
					<button ref={ closeButtonRef } type="button" tabIndex={ dir ? 0 : -1 } className="cliq-sl-directions__close" aria-label={ __( 'Close', 'cliq-store-locator' ) } onClick={ closeDirections }>×</button>
				</div>
				{ dir?.loading ? <div className="cliq-sl-directions__status" role="status">{ __( 'Calculating route…', 'cliq-store-locator' ) }</div> : null }
				{ dir?.error ? <div className="cliq-sl-directions__status is-error" role="alert">{ dir.error }</div> : null }
				{ /* The provider renders the origin, total distance/time and turn-by-turn
				     steps into this panel (Google's adp table or the OSRM list). */ }
				<div className="cliq-sl-directions__steps" ref={ panelRef } />
		</aside>
	);
	const originPromptPanel = originPrompt ? (
		<div className="cliq-sl-location-prompt cliq-sl-directions-origin-prompt" role="presentation" onMouseDown={ ( event ) => {
			if ( event.target === event.currentTarget && ! originPrompt.locating && ! originPrompt.submitting ) closeOriginPrompt();
		} }>
			<div className="cliq-sl-location-prompt__dialog cliq-sl-directions-origin-prompt__dialog" role="dialog" aria-modal="true" aria-label={ __( 'Choose your starting point', 'cliq-store-locator' ) }>
				<button type="button" className="cliq-sl-location-prompt__close" aria-label={ __( 'Close directions origin prompt', 'cliq-store-locator' ) } disabled={ originPrompt.locating || originPrompt.submitting } onClick={ closeOriginPrompt }>×</button>
				<div className="cliq-sl-location-prompt__icon" aria-hidden="true">
					<svg viewBox="0 0 24 24" focusable="false"><path d="M5 19 19 5M9 5h10v10"/><circle cx="6" cy="18" r="2"/></svg>
				</div>
				<h2>{ __( 'Choose your starting point', 'cliq-store-locator' ) }</h2>
				<p>{ __( 'Use your current location or enter the address you want directions from.', 'cliq-store-locator' ) }</p>
				<form className="cliq-sl-directions-origin-form" onSubmit={ submitOriginAddress }>
					<label className="cliq-sl-directions-origin-form__field">
						<span>{ __( 'From address', 'cliq-store-locator' ) }</span>
						<LocationAutocompleteInput
							className="cliq-sl-directions-origin-form__input"
							value={ originPrompt.address }
							placeholder={ __( 'Start typing an address…', 'cliq-store-locator' ) }
							country={ config?.restrictCountry || '' }
							onChange={ ( address ) => setOriginPrompt( ( current ) => current ? { ...current, address, selected: null, error: '' } : current ) }
							onPlaceSelected={ ( selected, address ) => setOriginPrompt( ( current ) => current ? { ...current, address, selected, error: '' } : current ) }
						/>
					</label>
					{ originPrompt.error ? <div className="cliq-sl-directions-origin-form__error" role="alert">{ originPrompt.error }</div> : null }
					<div className="cliq-sl-directions-origin-form__actions">
						<button type="button" className="cliq-sl-directions-origin-form__locate" disabled={ originPrompt.locating || originPrompt.submitting } onClick={ useCurrentOrigin }>
							{ originPrompt.locating ? __( 'Finding your location…', 'cliq-store-locator' ) : __( 'Use my location', 'cliq-store-locator' ) }
						</button>
						<button type="submit" className="cliq-sl-location-prompt__confirm" disabled={ originPrompt.locating || originPrompt.submitting }>
							{ originPrompt.submitting ? __( 'Finding address…', 'cliq-store-locator' ) : __( 'Get directions', 'cliq-store-locator' ) }
						</button>
					</div>
				</form>
			</div>
		</div>
	) : null;

	return (
		<div ref={ wrapRef } className={ `cliq-sl-w cliq-sl-w--map cliq-sl-map-wrap${ dir && ! directionsInResults ? ' is-directions-open' : '' }${ isFullscreen ? ' is-fullscreen' : '' }` }>
			{ /* Always mounted (hidden until a route is drawn) so the provider has a
			     panel node to render turn-by-turn steps into. */ }
			{ directionsHost ? createPortal( directionsPanel, directionsHost ) : directionsPanel }
			{ originPromptPanel }
			<div className="cliq-sl-map" ref={ elRef } role="region" aria-label={ __( 'Store locations map', 'cliq-store-locator' ) } />
			<button type="button" className="cliq-sl-map-fullscreen" aria-label={ isFullscreen ? __( 'Exit fullscreen map', 'cliq-store-locator' ) : __( 'View map fullscreen', 'cliq-store-locator' ) } aria-pressed={ isFullscreen } onClick={ toggleFullscreen }>
				{ isFullscreen ? (
					<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 3v6H3M15 3v6h6M9 21v-6H3M15 21v-6h6" /></svg>
				) : (
					<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 3H3v6M15 3h6v6M9 21H3v-6M15 21h6v-6" /></svg>
				) }
			</button>
			{ ws.showSearchOverlay && ws.overlaySearchFormId ? (
				<div className={ `cliq-sl-map-search-overlay is-${ ws.searchOverlayPosition || 'center' }` }>
					<SearchForm ws={ { searchFormId: ws.overlaySearchFormId } } />
				</div>
			) : null }
			{ ws.showMarkerNavigation ? (
				<div className={ `cliq-sl-marker-navs is-${ markerNavPosition }` }>
					<button type="button" className="cliq-sl-marker-nav cliq-sl-marker-nav--prev" disabled={ ! navigableResults.length } aria-label={ __( 'Previous store marker', 'cliq-store-locator' ) } onClick={ () => navigateMarker( -1 ) }>
						<WithIcon item={ { icon: ws.markerPrevIcon || 'chevron-left' } } />
					</button>
					<button type="button" className="cliq-sl-marker-nav cliq-sl-marker-nav--next" disabled={ ! navigableResults.length } aria-label={ __( 'Next store marker', 'cliq-store-locator' ) } onClick={ () => navigateMarker( 1 ) }>
						<WithIcon item={ { icon: ws.markerNextIcon || 'chevron-right' } } />
					</button>
				</div>
			) : null }
			{ locationPromptOpen ? (
				<div className="cliq-sl-location-prompt" role="presentation" onMouseDown={ ( event ) => {
					if ( event.target === event.currentTarget && ! locating ) setLocationPromptOpen( false );
				} }>
					<div className="cliq-sl-location-prompt__dialog" role="dialog" aria-label={ locationPromptSettings.locationPromptTitle || __( 'See stores near you', 'cliq-store-locator' ) }>
						<button type="button" className="cliq-sl-location-prompt__close" aria-label={ __( 'Close location prompt', 'cliq-store-locator' ) } disabled={ locating } onClick={ () => setLocationPromptOpen( false ) }>×</button>
						<div className="cliq-sl-location-prompt__icon" aria-hidden="true">
							<svg viewBox="0 0 24 24" focusable="false"><path d="M12 21s7-6.1 7-12a7 7 0 1 0-14 0c0 5.9 7 12 7 12Z"/><circle cx="12" cy="9" r="2.5"/></svg>
						</div>
						<h2>{ locationPromptSettings.locationPromptTitle || __( 'See stores near you', 'cliq-store-locator' ) }</h2>
						<p>{ locationPromptSettings.locationPromptMessage || __( 'Share your location to view the nearest stores and accurate distances.', 'cliq-store-locator' ) }</p>
						<button type="button" className="cliq-sl-location-prompt__confirm" disabled={ locating } onClick={ requestLocation }>
							{ locating ? __( 'Finding your location…', 'cliq-store-locator' ) : ( locationPromptSettings.locationPromptButton || __( 'Use my location', 'cliq-store-locator' ) ) }
						</button>
					</div>
				</div>
			) : null }
		</div>
	);
};
