import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Button, TextControl, Notice, Spinner } from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';
import { applyFilters } from '@cliqui/shared/hooks';
import Leaflet from 'leaflet';
import 'leaflet/dist/leaflet.css';

/** Leaflet is bundled into the admin entry; no third-party CDN is used. */
const ensureLeaflet = () => {
	if ( typeof window === 'undefined' ) {
		return Promise.reject( new Error( 'No window' ) );
	}

	return Promise.resolve( Leaflet );
};

let googleLoader = null;

/** Load the Google Maps JS API (once) for the given browser key. */
const ensureGoogle = ( apiKey ) => {
	if ( typeof window === 'undefined' ) {
		return Promise.reject( new Error( 'No window' ) );
	}
	if ( window.google?.maps ) {
		return Promise.resolve( window.google );
	}
	if ( googleLoader ) {
		return googleLoader;
	}

	googleLoader = new Promise( ( resolve, reject ) => {
		// With loading=async, google.maps is populated lazily — a global callback
		// fires only once it's ready, so we never touch g.maps too early.
		const cb = '__cliqGmapsReady';
		window[ cb ] = () => resolve( window.google );

		if ( document.querySelector( 'script[data-cliq-gmaps]' ) ) {
			if ( window.google?.maps ) {
				resolve( window.google );
			}
			return; // an in-flight script will fire the callback
		}
		const script = document.createElement( 'script' );
		script.src = `https://maps.googleapis.com/maps/api/js?key=${ encodeURIComponent( apiKey ) }&loading=async&callback=${ cb }`;
		script.async = true;
		script.defer = true;
		script.setAttribute( 'data-cliq-gmaps', '1' );
		script.onerror = () =>
			reject( new Error( __( 'Unable to load Google Maps.', 'cliq-store-locator' ) ) );
		document.head.appendChild( script );
	} );

	return googleLoader;
};

const toNumber = ( value, fallback = 0 ) => {
	const n = parseFloat( value );
	return Number.isNaN( n ) ? fallback : n;
};

const adminNamespace = () =>
	( typeof window !== 'undefined' &&
		window.CLIQTHEMES_UI?.app?.adminNamespace ) ||
	'cliqthemes-ui/v1';

// A teardrop pin as a divIcon — avoids Leaflet's broken default marker image
// paths under webpack and keeps the marker crisp at any zoom.
const pinHtml = `<svg width="30" height="42" viewBox="0 0 30 42" xmlns="http://www.w3.org/2000/svg"><path d="M15 0C6.7 0 0 6.7 0 15c0 10.5 15 27 15 27s15-16.5 15-27C30 6.7 23.3 0 15 0z" fill="#3858e9"/><circle cx="15" cy="15" r="6" fill="#fff"/></svg>`;

const MapField = ( { field, value, onChange, setFieldValue, formValues } ) => {
	const def = field.defaultLocation ?? { lat: 51.5074, lng: -0.1278, zoom: 12 };

	const composite = value && typeof value === 'object' ? value : {};
	const hasCoords =
		toNumber( composite.lat ) !== 0 || toNumber( composite.lng ) !== 0;

	const [ lat, setLat ] = useState( () =>
		hasCoords ? toNumber( composite.lat ) : toNumber( def.lat )
	);
	const [ lng, setLng ] = useState( () =>
		hasCoords ? toNumber( composite.lng ) : toNumber( def.lng )
	);
	const [ ready, setReady ] = useState( false );
	const [ mapError, setMapError ] = useState( '' );
	const [ geoBusy, setGeoBusy ] = useState( false );
	const [ geoError, setGeoError ] = useState( '' );

	// Use Google when it's the configured provider AND a browser key is present;
	// otherwise fall back to Leaflet + OpenStreetMap.
	const useGoogle = field.provider === 'google' && !! field.apiKey;

	const canvasRef = useRef( null );
	const mapRef = useRef( null );
	const markerRef = useRef( null );
	const kindRef = useRef( useGoogle ? 'google' : 'leaflet' );
	const onChangeRef = useRef( onChange );
	onChangeRef.current = onChange;
	// Latest sibling form values (address fields) for geocoding + auto-fill.
	const formValuesRef = useRef( formValues );
	formValuesRef.current = formValues;
	const setFieldValueRef = useRef( setFieldValue );
	setFieldValueRef.current = setFieldValue;

	// Push the composite up to the form (lat/lng only — address parts are their
	// own fields and persist independently).
	const commit = useCallback( ( nextLat, nextLng ) => {
		if ( typeof onChangeRef.current === 'function' ) {
			onChangeRef.current( { lat: nextLat, lng: nextLng } );
		}
	}, [] );

	// Move the marker + map and sync state/value.
	const place = useCallback(
		( nextLat, nextLng, { recenter = true } = {} ) => {
			setLat( nextLat );
			setLng( nextLng );
			commit( nextLat, nextLng );

			const map = mapRef.current;
			const marker = markerRef.current;
			if ( ! map || ! marker ) {
				return;
			}
			if ( kindRef.current === 'google' ) {
				marker.setPosition( { lat: nextLat, lng: nextLng } );
				if ( recenter ) {
					map.panTo( { lat: nextLat, lng: nextLng } );
				}
			} else {
				marker.setLatLng( [ nextLat, nextLng ] );
				if ( recenter ) {
					map.panTo( [ nextLat, nextLng ] );
				}
			}
		},
		[ commit ]
	);

	// Initialise the map once, using the configured provider.
	useEffect( () => {
		let cancelled = false;

		const fail = () => {
			if ( ! cancelled ) {
				setMapError(
					__(
						'The interactive map could not load. You can still enter coordinates manually below.',
						'cliq-store-locator'
					)
				);
			}
		};

		if ( useGoogle ) {
			ensureGoogle( field.apiKey )
				.then( ( g ) => {
					if ( cancelled || ! canvasRef.current || mapRef.current ) {
						return;
					}
					const map = new g.maps.Map( canvasRef.current, {
						center: { lat, lng },
						zoom: hasCoords ? 15 : toNumber( def.zoom, 12 ),
						mapTypeId: field.mapType || 'roadmap',
						mapTypeControl: false,
						streetViewControl: false,
						fullscreenControl: false,
						scrollwheel: false,
					} );
					const marker = new g.maps.Marker( {
						position: { lat, lng },
						map,
						draggable: field.draggable !== false,
					} );
					marker.addListener( 'dragend', ( e ) => {
						place( +e.latLng.lat().toFixed( 7 ), +e.latLng.lng().toFixed( 7 ), { recenter: false } );
					} );
					map.addListener( 'click', ( e ) => {
						place( +e.latLng.lat().toFixed( 7 ), +e.latLng.lng().toFixed( 7 ) );
					} );
					mapRef.current = map;
					markerRef.current = marker;
					kindRef.current = 'google';
					setReady( true );
				} )
				.catch( fail );

			return () => {
				cancelled = true;
				mapRef.current = null;
				markerRef.current = null;
			};
		}

		ensureLeaflet()
			.then( ( L ) => {
				if ( cancelled || ! canvasRef.current || mapRef.current ) {
					return;
				}

				const map = L.map( canvasRef.current, {
					center: [ lat, lng ],
					zoom: hasCoords ? 14 : toNumber( def.zoom, 12 ),
					scrollWheelZoom: false,
				} );

				L.tileLayer(
					'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
					{
						attribution: '© OpenStreetMap contributors',
						maxZoom: 19,
					}
				).addTo( map );

				const icon = L.divIcon( {
					className: 'cliqthemes-ui-map-pin',
					html: pinHtml,
					iconSize: [ 30, 42 ],
					iconAnchor: [ 15, 42 ],
				} );

				const marker = L.marker( [ lat, lng ], {
					draggable: field.draggable !== false,
					icon,
				} ).addTo( map );

				marker.on( 'dragend', () => {
					const p = marker.getLatLng();
					place( +p.lat.toFixed( 7 ), +p.lng.toFixed( 7 ), {
						recenter: false,
					} );
				} );

				map.on( 'click', ( e ) => {
					place( +e.latlng.lat.toFixed( 7 ), +e.latlng.lng.toFixed( 7 ) );
				} );

				mapRef.current = map;
				markerRef.current = marker;
				kindRef.current = 'leaflet';
				setReady( true );

				// Leaflet needs a size recalculation when revealed inside tabs.
				setTimeout( () => map.invalidateSize(), 80 );
			} )
			.catch( fail );

		return () => {
			cancelled = true;
			if ( mapRef.current && typeof mapRef.current.remove === 'function' ) {
				mapRef.current.remove();
			}
			mapRef.current = null;
			markerRef.current = null;
		};
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [] );

	// Build the geocode query from sibling fields named by the host config
	// (field.geocode.addressFields) — the framework knows no field names. A
	// `cliqui.geocode_query` filter lets extensions reshape the query.
	const addressQuery = () => {
		const fv = formValuesRef.current || {};
		const names = field.geocode?.addressFields || [];
		const base = names
			.map( ( name ) => ( fv[ name ] == null ? '' : String( fv[ name ] ).trim() ) )
			.filter( Boolean )
			.join( ', ' );
		return applyFilters( 'cliqui.geocode_query', base, { formValues: fv, field } );
	};

	// Geocode the address, drop the pin, and auto-fill blank fields. Which result
	// component fills which field is host config (field.geocode.fill: { target:
	// [componentKeys] }); a `cliqui.geocode_fill` filter lets extensions transform
	// the values (e.g. uppercase a country code) before they're written.
	const handleLocate = useCallback( async () => {
		const query = addressQuery();
		if ( query === '' ) {
			setGeoError( __( 'Enter an address above first.', 'cliq-store-locator' ) );
			return;
		}
		setGeoBusy( true );
		setGeoError( '' );
		try {
			const endpoint = field.geocode?.endpoint || `${ adminNamespace() }/admin/geocode`;
			const res = await apiFetch( {
				path: endpoint,
				method: 'POST',
				data: { address: query },
			} );
			const gLat = toNumber( res?.latitude );
			const gLng = toNumber( res?.longitude );
			if ( gLat === 0 && gLng === 0 ) {
				setGeoError( __( 'No match found for that address.', 'cliq-store-locator' ) );
				return;
			}
			place( gLat, gLng );
			if ( mapRef.current ) {
				mapRef.current.setZoom( 15 );
			}

			// Resolve fills from the host's component→field map, then let hooks adjust.
			const setField = setFieldValueRef.current;
			const fv = formValuesRef.current || {};
			const c = res?.components || {};
			const fillMap = field.geocode?.fill || {};
			let updates = {};
			Object.keys( fillMap ).forEach( ( target ) => {
				const keys = Array.isArray( fillMap[ target ] ) ? fillMap[ target ] : [ fillMap[ target ] ];
				for ( const k of keys ) {
					if ( c[ k ] ) {
						updates[ target ] = c[ k ];
						break;
					}
				}
			} );
			updates = applyFilters( 'cliqui.geocode_fill', updates, { components: c, formValues: fv, field } );
			if ( typeof setField === 'function' ) {
				Object.keys( updates ).forEach( ( target ) => {
					if ( updates[ target ] && ! String( fv[ target ] ?? '' ).trim() ) {
						setField( target, updates[ target ] );
					}
				} );
			}
		} catch ( e ) {
			setGeoError(
				__( 'Geocoding failed. Check the geocoder in Settings.', 'cliq-store-locator' )
			);
		} finally {
			setGeoBusy( false );
		}
	}, [ place ] );

	const handleLatInput = ( v ) => {
		const n = toNumber( v, lat );
		place( n, lng, { recenter: true } );
	};
	const handleLngInput = ( v ) => {
		const n = toNumber( v, lng );
		place( lat, n, { recenter: true } );
	};

	return (
		<div className="cliqthemes-ui-map-field">
			<div className="cliqthemes-ui-map-field__locate">
				<Button
					variant="secondary"
					className="cliqthemes-ui-button cliqthemes-ui-button--secondary"
					onClick={ handleLocate }
					isBusy={ geoBusy }
					disabled={ geoBusy }
				>
					{ __( 'Locate on map', 'cliq-store-locator' ) }
				</Button>
				<span className="cliqthemes-ui-field-help">
					{ __( 'Finds the address above and drops the pin. You can also click or drag on the map.', 'cliq-store-locator' ) }
				</span>
			</div>

			{ geoError && (
				<Notice status="warning" isDismissible onRemove={ () => setGeoError( '' ) }>
					{ geoError }
				</Notice>
			) }

			<div className="cliqthemes-ui-map-field__canvas-wrap">
				{ ! ready && ! mapError && (
					<div className="cliqthemes-ui-map-field__loading">
						<Spinner />
						<span>{ __( 'Loading map…', 'cliq-store-locator' ) }</span>
					</div>
				) }
				<div ref={ canvasRef } className="cliqthemes-ui-map-field__canvas" />
			</div>

			{ mapError && (
				<Notice status="warning" isDismissible={ false }>
					{ mapError }
				</Notice>
			) }

			<div className="cliqthemes-ui-map-field__coords">
				<TextControl
					__nextHasNoMarginBottom
					type="number"
					step="0.0000001"
					label={ __( 'Latitude', 'cliq-store-locator' ) }
					value={ lat }
					onChange={ handleLatInput }
				/>
				<TextControl
					__nextHasNoMarginBottom
					type="number"
					step="0.0000001"
					label={ __( 'Longitude', 'cliq-store-locator' ) }
					value={ lng }
					onChange={ handleLngInput }
				/>
			</div>

			{ field.help && (
				<p className="cliqthemes-ui-field-help">{ field.help }</p>
			) }
		</div>
	);
};

export default MapField;
