"use client"

import MapLibreGL, { type MarkerOptions, type PopupOptions } from "maplibre-gl"
import "maplibre-gl/dist/maplibre-gl.css"
import { Loader2, Locate, Maximize, Minus, Plus, X } from "lucide-react"
import {
	createContext,
	forwardRef,
	type ReactNode,
	useCallback,
	useContext,
	useEffect,
	useId,
	useImperativeHandle,
	useMemo,
	useRef,
	useState
} from "react"
import { createPortal } from "react-dom"

import { cn } from "@/lib/utils"

const defaultStyles = {
	dark: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
	light: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
}

type Theme = "light" | "dark"

// Check document class for theme (works with next-themes, etc.)
function getDocumentTheme(): Theme | null {
	if (typeof document === "undefined") return null
	if (document.documentElement.classList.contains("dark")) return "dark"
	if (document.documentElement.classList.contains("light")) return "light"
	return null
}

// Get system preference
function getSystemTheme(): Theme {
	if (typeof window === "undefined") return "light"
	return window.matchMedia("(prefers-color-scheme: dark)").matches
		? "dark"
		: "light"
}

function useResolvedTheme(themeProp?: "light" | "dark"): Theme {
	const [detectedTheme, setDetectedTheme] = useState<Theme>(
		() => getDocumentTheme() ?? getSystemTheme()
	)

	useEffect(() => {
		if (themeProp) return // Skip detection if theme is provided via prop

		// Watch for document class changes (e.g., next-themes toggling dark class)
		const observer = new MutationObserver(() => {
			const docTheme = getDocumentTheme()
			if (docTheme) {
				setDetectedTheme(docTheme)
			}
		})
		observer.observe(document.documentElement, {
			attributes: true,
			attributeFilter: ["class"]
		})

		// Also watch for system preference changes
		const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
		const handleSystemChange = (e: MediaQueryListEvent) => {
			// Only use system preference if no document class is set
			if (!getDocumentTheme()) {
				setDetectedTheme(e.matches ? "dark" : "light")
			}
		}
		mediaQuery.addEventListener("change", handleSystemChange)

		return () => {
			observer.disconnect()
			mediaQuery.removeEventListener("change", handleSystemChange)
		}
	}, [themeProp])

	return themeProp ?? detectedTheme
}

type MapContextValue = {
	map: MapLibreGL.Map | null
	isLoaded: boolean
}

const MapContext = createContext<MapContextValue | null>(null)

function useMap() {
	const context = useContext(MapContext)
	if (!context) {
		throw new Error("useMap must be used within a Map component")
	}
	return context
}

/** Map viewport state */
type MapViewport = {
	/** Center coordinates [longitude, latitude] */
	center: [number, number]
	/** Zoom level */
	zoom: number
	/** Bearing (rotation) in degrees */
	bearing: number
	/** Pitch (tilt) in degrees */
	pitch: number
}

type MapStyleOption = string | MapLibreGL.StyleSpecification

type MapRef = MapLibreGL.Map

type MapProps = {
	children?: ReactNode
	/** Additional CSS classes for the map container */
	className?: string
	/**
	 * Theme for the map. If not provided, automatically detects system preference.
	 * Pass your theme value here.
	 */
	theme?: Theme
	/** Custom map styles for light and dark themes. Overrides the default Carto styles. */
	styles?: {
		light?: MapStyleOption
		dark?: MapStyleOption
	}
	/** Map projection type. Use `{ type: "globe" }` for 3D globe view. */
	projection?: MapLibreGL.ProjectionSpecification
	/**
	 * Controlled viewport. When provided with onViewportChange,
	 * the map becomes controlled and viewport is driven by this prop.
	 */
	viewport?: Partial<MapViewport>
	/**
	 * Callback fired continuously as the viewport changes (pan, zoom, rotate, pitch).
	 * Can be used standalone to observe changes, or with `viewport` prop
	 * to enable controlled mode where the map viewport is driven by your state.
	 */
	onViewportChange?: (viewport: MapViewport) => void
} & Omit<MapLibreGL.MapOptions, "container" | "style">

function DefaultLoader() {
	return (
		<div className="absolute inset-0 flex items-center justify-center">
			<div className="flex gap-1">
				<span className="size-1.5 rounded-full bg-muted-foreground/60 animate-pulse" />
				<span className="size-1.5 rounded-full bg-muted-foreground/60 animate-pulse [animation-delay:150ms]" />
				<span className="size-1.5 rounded-full bg-muted-foreground/60 animate-pulse [animation-delay:300ms]" />
			</div>
		</div>
	)
}

function getViewport(map: MapLibreGL.Map): MapViewport {
	const center = map.getCenter()
	return {
		center: [center.lng, center.lat],
		zoom: map.getZoom(),
		bearing: map.getBearing(),
		pitch: map.getPitch()
	}
}

const Map = forwardRef<MapRef, MapProps>(function Map(
	{
		children,
		className,
		theme: themeProp,
		styles,
		projection,
		viewport,
		onViewportChange,
		...props
	},
	ref
) {
	const containerRef = useRef<HTMLDivElement>(null)
	const [mapInstance, setMapInstance] = useState<MapLibreGL.Map | null>(null)
	const [isLoaded, setIsLoaded] = useState(false)
	const [isStyleLoaded, setIsStyleLoaded] = useState(false)
	const currentStyleRef = useRef<MapStyleOption | null>(null)
	const styleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
	const internalUpdateRef = useRef(false)
	const resolvedTheme = useResolvedTheme(themeProp)

	const isControlled = viewport !== undefined && onViewportChange !== undefined

	const onViewportChangeRef = useRef(onViewportChange)
	onViewportChangeRef.current = onViewportChange

	const mapStyles = useMemo(
		() => ({
			dark: styles?.dark ?? defaultStyles.dark,
			light: styles?.light ?? defaultStyles.light
		}),
		[styles]
	)

	// Expose the map instance to the parent component
	useImperativeHandle(ref, () => mapInstance as MapLibreGL.Map, [mapInstance])

	const clearStyleTimeout = useCallback(() => {
		if (styleTimeoutRef.current) {
			clearTimeout(styleTimeoutRef.current)
			styleTimeoutRef.current = null
		}
	}, [])

	// Initialize the map
	useEffect(() => {
		if (!containerRef.current) return

		const initialStyle =
			resolvedTheme === "dark" ? mapStyles.dark : mapStyles.light
		currentStyleRef.current = initialStyle

		const map = new MapLibreGL.Map({
			container: containerRef.current,
			style: initialStyle,
			renderWorldCopies: false,
			attributionControl: {
				compact: true
			},
			...props,
			...viewport
		})

		const styleDataHandler = () => {
			clearStyleTimeout()
			// Delay to ensure style is fully processed before allowing layer operations
			// This is a workaround to avoid race conditions with the style loading
			// else we have to force update every layer on setStyle change
			styleTimeoutRef.current = setTimeout(() => {
				setIsStyleLoaded(true)
				if (projection) {
					map.setProjection(projection)
				}
			}, 100)
		}
		const loadHandler = () => setIsLoaded(true)

		// Viewport change handler - skip if triggered by internal update
		const handleMove = () => {
			if (internalUpdateRef.current) return
			onViewportChangeRef.current?.(getViewport(map))
		}

		map.on("load", loadHandler)
		map.on("styledata", styleDataHandler)
		map.on("move", handleMove)
		setMapInstance(map)

		return () => {
			clearStyleTimeout()
			map.off("load", loadHandler)
			map.off("styledata", styleDataHandler)
			map.off("move", handleMove)
			map.remove()
			setIsLoaded(false)
			setIsStyleLoaded(false)
			setMapInstance(null)
		}
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [])

	// Sync controlled viewport to map
	useEffect(() => {
		if (!mapInstance || !isControlled || !viewport) return
		if (mapInstance.isMoving()) return

		const current = getViewport(mapInstance)
		const next = {
			center: viewport.center ?? current.center,
			zoom: viewport.zoom ?? current.zoom,
			bearing: viewport.bearing ?? current.bearing,
			pitch: viewport.pitch ?? current.pitch
		}

		if (
			next.center[0] === current.center[0] &&
			next.center[1] === current.center[1] &&
			next.zoom === current.zoom &&
			next.bearing === current.bearing &&
			next.pitch === current.pitch
		) {
			return
		}

		internalUpdateRef.current = true
		mapInstance.jumpTo(next)
		internalUpdateRef.current = false
	}, [mapInstance, isControlled, viewport])

	// Handle style change
	useEffect(() => {
		if (!mapInstance || !resolvedTheme) return

		const newStyle = resolvedTheme === "dark" ? mapStyles.dark : mapStyles.light

		if (currentStyleRef.current === newStyle) return

		clearStyleTimeout()
		currentStyleRef.current = newStyle
		setIsStyleLoaded(false)

		mapInstance.setStyle(newStyle, { diff: true })
	}, [mapInstance, resolvedTheme, mapStyles, clearStyleTimeout])

	const contextValue = useMemo(
		() => ({
			map: mapInstance,
			isLoaded: isLoaded && isStyleLoaded
		}),
		[mapInstance, isLoaded, isStyleLoaded]
	)

	return (
		<MapContext.Provider value={contextValue}>
			<div
				ref={containerRef}
				className={cn("relative w-full h-full", className)}
			>
				{!isLoaded && <DefaultLoader />}
				{/* SSR-safe: children render only when map is loaded on client */}
				{mapInstance && children}
			</div>
		</MapContext.Provider>
	)
})

type MarkerContextValue = {
	marker: MapLibreGL.Marker
	map: MapLibreGL.Map | null
}

const MarkerContext = createContext<MarkerContextValue | null>(null)

function useMarkerContext() {
	const context = useContext(MarkerContext)
	if (!context) {
		throw new Error("Marker components must be used within MapMarker")
	}
	return context
}

type MapMarkerProps = {
	/** Longitude coordinate for marker position */
	longitude: number
	/** Latitude coordinate for marker position */
	latitude: number
	/** Marker subcomponents (MarkerContent, MarkerPopup, MarkerTooltip, MarkerLabel) */
	children: ReactNode
	/** Callback when marker is clicked */
	onClick?: (e: MouseEvent) => void
	/** Callback when mouse enters marker */
	onMouseEnter?: (e: MouseEvent) => void
	/** Callback when mouse leaves marker */
	onMouseLeave?: (e: MouseEvent) => void
	/** Callback when marker drag starts (requires draggable: true) */
	onDragStart?: (lngLat: { lng: number; lat: number }) => void
	/** Callback during marker drag (requires draggable: true) */
	onDrag?: (lngLat: { lng: number; lat: number }) => void
	/** Callback when marker drag ends (requires draggable: true) */
	onDragEnd?: (lngLat: { lng: number; lat: number }) => void
} & Omit<MarkerOptions, "element">

function MapMarker({
	longitude,
	latitude,
	children,
	onClick,
	onMouseEnter,
	onMouseLeave,
	onDragStart,
	onDrag,
	onDragEnd,
	draggable = false,
	...markerOptions
}: MapMarkerProps) {
	const { map } = useMap()

	const callbacksRef = useRef({
		onClick,
		onMouseEnter,
		onMouseLeave,
		onDragStart,
		onDrag,
		onDragEnd
	})
	callbacksRef.current = {
		onClick,
		onMouseEnter,
		onMouseLeave,
		onDragStart,
		onDrag,
		onDragEnd
	}

	const marker = useMemo(() => {
		const markerInstance = new MapLibreGL.Marker({
			...markerOptions,
			element: document.createElement("div"),
			draggable
		}).setLngLat([longitude, latitude])

		const handleClick = (e: MouseEvent) => callbacksRef.current.onClick?.(e)
		const handleMouseEnter = (e: MouseEvent) =>
			callbacksRef.current.onMouseEnter?.(e)
		const handleMouseLeave = (e: MouseEvent) =>
			callbacksRef.current.onMouseLeave?.(e)

		markerInstance.getElement()?.addEventListener("click", handleClick)
		markerInstance
			.getElement()
			?.addEventListener("mouseenter", handleMouseEnter)
		markerInstance
			.getElement()
			?.addEventListener("mouseleave", handleMouseLeave)

		const handleDragStart = () => {
			const lngLat = markerInstance.getLngLat()
			callbacksRef.current.onDragStart?.({ lng: lngLat.lng, lat: lngLat.lat })
		}
		const handleDrag = () => {
			const lngLat = markerInstance.getLngLat()
			callbacksRef.current.onDrag?.({ lng: lngLat.lng, lat: lngLat.lat })
		}
		const handleDragEnd = () => {
			const lngLat = markerInstance.getLngLat()
			callbacksRef.current.onDragEnd?.({ lng: lngLat.lng, lat: lngLat.lat })
		}

		markerInstance.on("dragstart", handleDragStart)
		markerInstance.on("drag", handleDrag)
		markerInstance.on("dragend", handleDragEnd)

		return markerInstance

		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [])

	useEffect(() => {
		if (!map) return

		marker.addTo(map)

		return () => {
			marker.remove()
		}

		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [map])

	if (
		marker.getLngLat().lng !== longitude ||
		marker.getLngLat().lat !== latitude
	) {
		marker.setLngLat([longitude, latitude])
	}
	if (marker.isDraggable() !== draggable) {
		marker.setDraggable(draggable)
	}

	const currentOffset = marker.getOffset()
	const newOffset = markerOptions.offset ?? [0, 0]
	const [newOffsetX, newOffsetY] = Array.isArray(newOffset)
		? newOffset
		: [newOffset.x, newOffset.y]
	if (currentOffset.x !== newOffsetX || currentOffset.y !== newOffsetY) {
		marker.setOffset(newOffset)
	}

	if (marker.getRotation() !== markerOptions.rotation) {
		marker.setRotation(markerOptions.rotation ?? 0)
	}
	if (marker.getRotationAlignment() !== markerOptions.rotationAlignment) {
		marker.setRotationAlignment(markerOptions.rotationAlignment ?? "auto")
	}
	if (marker.getPitchAlignment() !== markerOptions.pitchAlignment) {
		marker.setPitchAlignment(markerOptions.pitchAlignment ?? "auto")
	}

	return (
		<MarkerContext.Provider value={{ marker, map }}>
			{children}
		</MarkerContext.Provider>
	)
}

type MarkerContentProps = {
	/** Custom marker content. Defaults to a blue dot if not provided */
	children?: ReactNode
	/** Additional CSS classes for the marker container */
	className?: string
}

function MarkerContent({ children, className }: MarkerContentProps) {
	const { marker } = useMarkerContext()

	return createPortal(
		<div className={cn("relative cursor-pointer", className)}>
			{children || <DefaultMarkerIcon />}
		</div>,
		marker.getElement()
	)
}

function DefaultMarkerIcon() {
	return (
		<div className="relative h-4 w-4 rounded-full border-2 border-white bg-blue-500 shadow-lg" />
	)
}

type MarkerPopupProps = {
	/** Popup content */
	children: ReactNode
	/** Additional CSS classes for the popup container */
	className?: string
	/** Show a close button in the popup (default: false) */
	closeButton?: boolean
} & Omit<PopupOptions, "className" | "closeButton">

function MarkerPopup({
	children,
	className,
	closeButton = false,
	...popupOptions
}: MarkerPopupProps) {
	const { marker, map } = useMarkerContext()
	const container = useMemo(() => document.createElement("div"), [])
	const prevPopupOptions = useRef(popupOptions)

	const popup = useMemo(() => {
		const popupInstance = new MapLibreGL.Popup({
			offset: 16,
			...popupOptions,
			closeButton: false
		})
			.setMaxWidth("none")
			.setDOMContent(container)

		return popupInstance
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [])

	useEffect(() => {
		if (!map) return

		popup.setDOMContent(container)
		marker.setPopup(popup)

		return () => {
			marker.setPopup(null)
		}
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [map])

	if (popup.isOpen()) {
		const prev = prevPopupOptions.current

		if (prev.offset !== popupOptions.offset) {
			popup.setOffset(popupOptions.offset ?? 16)
		}
		if (prev.maxWidth !== popupOptions.maxWidth && popupOptions.maxWidth) {
			popup.setMaxWidth(popupOptions.maxWidth ?? "none")
		}

		prevPopupOptions.current = popupOptions
	}

	const handleClose = () => popup.remove()

	return createPortal(
		<div
			className={cn(
				"relative rounded-md border bg-popover p-3 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95",
				className
			)}
		>
			{closeButton && (
				<button
					type="button"
					onClick={handleClose}
					className="absolute top-1 right-1 z-10 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
					aria-label="Close popup"
				>
					<X className="h-4 w-4" />
					<span className="sr-only">Close</span>
				</button>
			)}
			{children}
		</div>,
		container
	)
}

type MarkerTooltipProps = {
	/** Tooltip content */
	children: ReactNode
	/** Additional CSS classes for the tooltip container */
	className?: string
} & Omit<PopupOptions, "className" | "closeButton" | "closeOnClick">

function MarkerTooltip({
	children,
	className,
	...popupOptions
}: MarkerTooltipProps) {
	const { marker, map } = useMarkerContext()
	const container = useMemo(() => document.createElement("div"), [])
	const prevTooltipOptions = useRef(popupOptions)

	const tooltip = useMemo(() => {
		const tooltipInstance = new MapLibreGL.Popup({
			offset: 16,
			...popupOptions,
			closeOnClick: true,
			closeButton: false
		}).setMaxWidth("none")

		return tooltipInstance
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [])

	useEffect(() => {
		if (!map) return

		tooltip.setDOMContent(container)

		const handleMouseEnter = () => {
			tooltip.setLngLat(marker.getLngLat()).addTo(map)
		}
		const handleMouseLeave = () => tooltip.remove()

		marker.getElement()?.addEventListener("mouseenter", handleMouseEnter)
		marker.getElement()?.addEventListener("mouseleave", handleMouseLeave)

		return () => {
			marker.getElement()?.removeEventListener("mouseenter", handleMouseEnter)
			marker.getElement()?.removeEventListener("mouseleave", handleMouseLeave)
			tooltip.remove()
		}
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [map])

	if (tooltip.isOpen()) {
		const prev = prevTooltipOptions.current

		if (prev.offset !== popupOptions.offset) {
			tooltip.setOffset(popupOptions.offset ?? 16)
		}
		if (prev.maxWidth !== popupOptions.maxWidth && popupOptions.maxWidth) {
			tooltip.setMaxWidth(popupOptions.maxWidth ?? "none")
		}

		prevTooltipOptions.current = popupOptions
	}

	return createPortal(
		<div
			className={cn(
				"rounded-md bg-foreground px-2 py-1 text-xs text-background shadow-md animate-in fade-in-0 zoom-in-95",
				className
			)}
		>
			{children}
		</div>,
		container
	)
}

type MarkerLabelProps = {
	/** Label text content */
	children: ReactNode
	/** Additional CSS classes for the label */
	className?: string
	/** Position of the label relative to the marker (default: "top") */
	position?: "top" | "bottom"
}

function MarkerLabel({
	children,
	className,
	position = "top"
}: MarkerLabelProps) {
	const positionClasses = {
		top: "bottom-full mb-1",
		bottom: "top-full mt-1"
	}

	return (
		<div
			className={cn(
				"absolute left-1/2 -translate-x-1/2 whitespace-nowrap",
				"text-[10px] font-medium text-foreground",
				positionClasses[position],
				className
			)}
		>
			{children}
		</div>
	)
}

type MapControlsProps = {
	/** Position of the controls on the map (default: "bottom-right") */
	position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"
	/** Show zoom in/out buttons (default: true) */
	showZoom?: boolean
	/** Show compass button to reset bearing (default: false) */
	showCompass?: boolean
	/** Show locate button to find user's location (default: false) */
	showLocate?: boolean
	/** Show fullscreen toggle button (default: false) */
	showFullscreen?: boolean
	/** Additional CSS classes for the controls container */
	className?: string
	/** Callback with user coordinates when located */
	onLocate?: (coords: { longitude: number; latitude: number }) => void
}

const positionClasses = {
	"top-left": "top-2 left-2",
	"top-right": "top-2 right-2",
	"bottom-left": "bottom-2 left-2",
	"bottom-right": "bottom-10 right-2"
}

function ControlGroup({ children }: { children: React.ReactNode }) {
	return (
		<div className="flex flex-col rounded-md border border-border bg-background shadow-sm overflow-hidden [&>button:not(:last-child)]:border-b [&>button:not(:last-child)]:border-border">
			{children}
		</div>
	)
}

function ControlButton({
	onClick,
	label,
	children,
	disabled = false
}: {
	onClick: () => void
	label: string
	children: React.ReactNode
	disabled?: boolean
}) {
	return (
		<button
			onClick={onClick}
			aria-label={label}
			type="button"
			className={cn(
				"flex items-center justify-center size-8 hover:bg-accent dark:hover:bg-accent/40 transition-colors",
				disabled && "opacity-50 pointer-events-none cursor-not-allowed"
			)}
			disabled={disabled}
		>
			{children}
		</button>
	)
}

function MapControls({
	position = "bottom-right",
	showZoom = true,
	showCompass = false,
	showLocate = false,
	showFullscreen = false,
	className,
	onLocate
}: MapControlsProps) {
	const { map } = useMap()
	const [waitingForLocation, setWaitingForLocation] = useState(false)

	const handleZoomIn = useCallback(() => {
		map?.zoomTo(map.getZoom() + 1, { duration: 300 })
	}, [map])

	const handleZoomOut = useCallback(() => {
		map?.zoomTo(map.getZoom() - 1, { duration: 300 })
	}, [map])

	const handleResetBearing = useCallback(() => {
		map?.resetNorthPitch({ duration: 300 })
	}, [map])

	const handleLocate = useCallback(() => {
		setWaitingForLocation(true)
		if ("geolocation" in navigator) {
			navigator.geolocation.getCurrentPosition(
				(pos) => {
					const coords = {
						longitude: pos.coords.longitude,
						latitude: pos.coords.latitude
					}
					map?.flyTo({
						center: [coords.longitude, coords.latitude],
						zoom: 14,
						duration: 1500
					})
					onLocate?.(coords)
					setWaitingForLocation(false)
				},
				(error) => {
					console.error("Error getting location:", error)
					setWaitingForLocation(false)
				}
			)
		}
	}, [map, onLocate])

	const handleFullscreen = useCallback(() => {
		const container = map?.getContainer()
		if (!container) return
		if (document.fullscreenElement) {
			document.exitFullscreen()
		} else {
			container.requestFullscreen()
		}
	}, [map])

	return (
		<div
			className={cn(
				"absolute z-10 flex flex-col gap-1.5",
				positionClasses[position],
				className
			)}
		>
			{showZoom && (
				<ControlGroup>
					<ControlButton onClick={handleZoomIn} label="Zoom in">
						<Plus className="size-4" />
					</ControlButton>
					<ControlButton onClick={handleZoomOut} label="Zoom out">
						<Minus className="size-4" />
					</ControlButton>
				</ControlGroup>
			)}
			{showCompass && (
				<ControlGroup>
					<CompassButton onClick={handleResetBearing} />
				</ControlGroup>
			)}
			{showLocate && (
				<ControlGroup>
					<ControlButton
						onClick={handleLocate}
						label="Find my location"
						disabled={waitingForLocation}
					>
						{waitingForLocation ? (
							<Loader2 className="size-4 animate-spin" />
						) : (
							<Locate className="size-4" />
						)}
					</ControlButton>
				</ControlGroup>
			)}
			{showFullscreen && (
				<ControlGroup>
					<ControlButton onClick={handleFullscreen} label="Toggle fullscreen">
						<Maximize className="size-4" />
					</ControlButton>
				</ControlGroup>
			)}
		</div>
	)
}

function CompassButton({ onClick }: { onClick: () => void }) {
	const { map } = useMap()
	const compassRef = useRef<SVGSVGElement>(null)

	useEffect(() => {
		if (!map || !compassRef.current) return

		const compass = compassRef.current

		const updateRotation = () => {
			const bearing = map.getBearing()
			const pitch = map.getPitch()
			compass.style.transform = `rotateX(${pitch}deg) rotateZ(${-bearing}deg)`
		}

		map.on("rotate", updateRotation)
		map.on("pitch", updateRotation)
		updateRotation()

		return () => {
			map.off("rotate", updateRotation)
			map.off("pitch", updateRotation)
		}
	}, [map])

	return (
		<ControlButton onClick={onClick} label="Reset bearing to north">
			<svg
				ref={compassRef}
				viewBox="0 0 24 24"
				className="size-5 transition-transform duration-200"
				style={{ transformStyle: "preserve-3d" }}
			>
				<path d="M12 2L16 12H12V2Z" className="fill-red-500" />
				<path d="M12 2L8 12H12V2Z" className="fill-red-300" />
				<path d="M12 22L16 12H12V22Z" className="fill-muted-foreground/60" />
				<path d="M12 22L8 12H12V22Z" className="fill-muted-foreground/30" />
			</svg>
		</ControlButton>
	)
}

type MapPopupProps = {
	/** Longitude coordinate for popup position */
	longitude: number
	/** Latitude coordinate for popup position */
	latitude: number
	/** Callback when popup is closed */
	onClose?: () => void
	/** Popup content */
	children: ReactNode
	/** Additional CSS classes for the popup container */
	className?: string
	/** Show a close button in the popup (default: false) */
	closeButton?: boolean
} & Omit<PopupOptions, "className" | "closeButton">

function MapPopup({
	longitude,
	latitude,
	onClose,
	children,
	className,
	closeButton = false,
	...popupOptions
}: MapPopupProps) {
	const { map } = useMap()
	const popupOptionsRef = useRef(popupOptions)
	const onCloseRef = useRef(onClose)
	onCloseRef.current = onClose
	const container = useMemo(() => document.createElement("div"), [])

	const popup = useMemo(() => {
		const popupInstance = new MapLibreGL.Popup({
			offset: 16,
			...popupOptions,
			closeButton: false
		})
			.setMaxWidth("none")
			.setLngLat([longitude, latitude])

		return popupInstance
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [])

	useEffect(() => {
		if (!map) return

		const onCloseProp = () => onCloseRef.current?.()

		popup.on("close", onCloseProp)

		popup.setDOMContent(container)
		popup.addTo(map)

		return () => {
			popup.off("close", onCloseProp)
			if (popup.isOpen()) {
				popup.remove()
			}
		}
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [map])

	if (popup.isOpen()) {
		const prev = popupOptionsRef.current

		if (
			popup.getLngLat().lng !== longitude ||
			popup.getLngLat().lat !== latitude
		) {
			popup.setLngLat([longitude, latitude])
		}

		if (prev.offset !== popupOptions.offset) {
			popup.setOffset(popupOptions.offset ?? 16)
		}
		if (prev.maxWidth !== popupOptions.maxWidth && popupOptions.maxWidth) {
			popup.setMaxWidth(popupOptions.maxWidth ?? "none")
		}
		popupOptionsRef.current = popupOptions
	}

	const handleClose = () => {
		popup.remove()
	}

	return createPortal(
		<div
			className={cn(
				"relative rounded-md border bg-popover p-3 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95",
				className
			)}
		>
			{closeButton && (
				<button
					type="button"
					onClick={handleClose}
					className="absolute top-1 right-1 z-10 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
					aria-label="Close popup"
				>
					<X className="h-4 w-4" />
					<span className="sr-only">Close</span>
				</button>
			)}
			{children}
		</div>,
		container
	)
}

type MapRouteProps = {
	/** Optional unique identifier for the route layer */
	id?: string
	/** Array of [longitude, latitude] coordinate pairs defining the route */
	coordinates: [number, number][]
	/** Line color as CSS color value (default: "#4285F4") */
	color?: string
	/** Line width in pixels (default: 3) */
	width?: number
	/** Line opacity from 0 to 1 (default: 0.8) */
	opacity?: number
	/** Dash pattern [dash length, gap length] for dashed lines */
	dashArray?: [number, number]
	/** Callback when the route line is clicked */
	onClick?: () => void
	/** Callback when mouse enters the route line */
	onMouseEnter?: () => void
	/** Callback when mouse leaves the route line */
	onMouseLeave?: () => void
	/** Whether the route is interactive - shows pointer cursor on hover (default: true) */
	interactive?: boolean
}

function MapRoute({
	id: propId,
	coordinates,
	color = "#4285F4",
	width = 3,
	opacity = 0.8,
	dashArray,
	onClick,
	onMouseEnter,
	onMouseLeave,
	interactive = true
}: MapRouteProps) {
	const { map, isLoaded } = useMap()
	const autoId = useId()
	const id = propId ?? autoId
	const sourceId = `route-source-${id}`
	const layerId = `route-layer-${id}`

	// Add source and layer on mount
	useEffect(() => {
		if (!isLoaded || !map) return

		map.addSource(sourceId, {
			type: "geojson",
			data: {
				type: "Feature",
				properties: {},
				geometry: { type: "LineString", coordinates: [] }
			}
		})

		map.addLayer({
			id: layerId,
			type: "line",
			source: sourceId,
			layout: { "line-join": "round", "line-cap": "round" },
			paint: {
				"line-color": color,
				"line-width": width,
				"line-opacity": opacity,
				...(dashArray && { "line-dasharray": dashArray })
			}
		})

		return () => {
			try {
				if (map.getLayer(layerId)) map.removeLayer(layerId)
				if (map.getSource(sourceId)) map.removeSource(sourceId)
			} catch {
				// ignore
			}
		}
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [isLoaded, map])

	// When coordinates change, update the source data
	useEffect(() => {
		if (!isLoaded || !map || coordinates.length < 2) return

		const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource
		if (source) {
			source.setData({
				type: "Feature",
				properties: {},
				geometry: { type: "LineString", coordinates }
			})
		}
	}, [isLoaded, map, coordinates, sourceId])

	useEffect(() => {
		if (!isLoaded || !map || !map.getLayer(layerId)) return

		map.setPaintProperty(layerId, "line-color", color)
		map.setPaintProperty(layerId, "line-width", width)
		map.setPaintProperty(layerId, "line-opacity", opacity)
		if (dashArray) {
			map.setPaintProperty(layerId, "line-dasharray", dashArray)
		}
	}, [isLoaded, map, layerId, color, width, opacity, dashArray])

	// Handle click and hover events
	useEffect(() => {
		if (!isLoaded || !map || !interactive) return

		const handleClick = () => {
			onClick?.()
		}
		const handleMouseEnter = () => {
			map.getCanvas().style.cursor = "pointer"
			onMouseEnter?.()
		}
		const handleMouseLeave = () => {
			map.getCanvas().style.cursor = ""
			onMouseLeave?.()
		}

		map.on("click", layerId, handleClick)
		map.on("mouseenter", layerId, handleMouseEnter)
		map.on("mouseleave", layerId, handleMouseLeave)

		return () => {
			map.off("click", layerId, handleClick)
			map.off("mouseenter", layerId, handleMouseEnter)
			map.off("mouseleave", layerId, handleMouseLeave)
		}
	}, [isLoaded, map, layerId, onClick, onMouseEnter, onMouseLeave, interactive])

	return null
}

type MapClusterLayerProps<
	P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties
> = {
	/** GeoJSON FeatureCollection data or URL to fetch GeoJSON from */
	data: string | GeoJSON.FeatureCollection<GeoJSON.Point, P>
	/** Maximum zoom level to cluster points on (default: 14) */
	clusterMaxZoom?: number
	/** Radius of each cluster when clustering points in pixels (default: 50) */
	clusterRadius?: number
	/** Colors for cluster circles: [small, medium, large] based on point count (default: ["#22c55e", "#eab308", "#ef4444"]) */
	clusterColors?: [string, string, string]
	/** Point count thresholds for color/size steps: [medium, large] (default: [100, 750]) */
	clusterThresholds?: [number, number]
	/** Color for unclustered individual points (default: "#3b82f6") */
	pointColor?: string
	/** Callback when an unclustered point is clicked */
	onPointClick?: (
		feature: GeoJSON.Feature<GeoJSON.Point, P>,
		coordinates: [number, number]
	) => void
	/** Callback when a cluster is clicked. If not provided, zooms into the cluster */
	onClusterClick?: (
		clusterId: number,
		coordinates: [number, number],
		pointCount: number
	) => void
}

function MapClusterLayer<
	P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties
>({
	data,
	clusterMaxZoom = 14,
	clusterRadius = 50,
	clusterColors = ["#22c55e", "#eab308", "#ef4444"],
	clusterThresholds = [100, 750],
	pointColor = "#3b82f6",
	onPointClick,
	onClusterClick
}: MapClusterLayerProps<P>) {
	const { map, isLoaded } = useMap()
	const id = useId()
	const sourceId = `cluster-source-${id}`
	const clusterLayerId = `clusters-${id}`
	const clusterCountLayerId = `cluster-count-${id}`
	const unclusteredLayerId = `unclustered-point-${id}`

	const stylePropsRef = useRef({
		clusterColors,
		clusterThresholds,
		pointColor
	})

	// Add source and layers on mount
	useEffect(() => {
		if (!isLoaded || !map) return

		// Add clustered GeoJSON source
		map.addSource(sourceId, {
			type: "geojson",
			data,
			cluster: true,
			clusterMaxZoom,
			clusterRadius
		})

		// Add cluster circles layer
		map.addLayer({
			id: clusterLayerId,
			type: "circle",
			source: sourceId,
			filter: ["has", "point_count"],
			paint: {
				"circle-color": [
					"step",
					["get", "point_count"],
					clusterColors[0],
					clusterThresholds[0],
					clusterColors[1],
					clusterThresholds[1],
					clusterColors[2]
				],
				"circle-radius": [
					"step",
					["get", "point_count"],
					20,
					clusterThresholds[0],
					30,
					clusterThresholds[1],
					40
				],
				"circle-stroke-width": 1,
				"circle-stroke-color": "#fff",
				"circle-opacity": 0.85
			}
		})

		// Add cluster count text layer
		map.addLayer({
			id: clusterCountLayerId,
			type: "symbol",
			source: sourceId,
			filter: ["has", "point_count"],
			layout: {
				"text-field": "{point_count_abbreviated}",
				"text-font": ["Open Sans"],
				"text-size": 12
			},
			paint: {
				"text-color": "#fff"
			}
		})

		// Add unclustered point layer
		map.addLayer({
			id: unclusteredLayerId,
			type: "circle",
			source: sourceId,
			filter: ["!", ["has", "point_count"]],
			paint: {
				"circle-color": pointColor,
				"circle-radius": 5,
				"circle-stroke-width": 2,
				"circle-stroke-color": "#fff"
			}
		})

		return () => {
			try {
				if (map.getLayer(clusterCountLayerId))
					map.removeLayer(clusterCountLayerId)
				if (map.getLayer(unclusteredLayerId))
					map.removeLayer(unclusteredLayerId)
				if (map.getLayer(clusterLayerId)) map.removeLayer(clusterLayerId)
				if (map.getSource(sourceId)) map.removeSource(sourceId)
			} catch {
				// ignore
			}
		}
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [isLoaded, map, sourceId])

	// Update source data when data prop changes (only for non-URL data)
	useEffect(() => {
		if (!isLoaded || !map || typeof data === "string") return

		const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource
		if (source) {
			source.setData(data)
		}
	}, [isLoaded, map, data, sourceId])

	// Update layer styles when props change
	useEffect(() => {
		if (!isLoaded || !map) return

		const prev = stylePropsRef.current
		const colorsChanged =
			prev.clusterColors !== clusterColors ||
			prev.clusterThresholds !== clusterThresholds

		// Update cluster layer colors and sizes
		if (map.getLayer(clusterLayerId) && colorsChanged) {
			map.setPaintProperty(clusterLayerId, "circle-color", [
				"step",
				["get", "point_count"],
				clusterColors[0],
				clusterThresholds[0],
				clusterColors[1],
				clusterThresholds[1],
				clusterColors[2]
			])
			map.setPaintProperty(clusterLayerId, "circle-radius", [
				"step",
				["get", "point_count"],
				20,
				clusterThresholds[0],
				30,
				clusterThresholds[1],
				40
			])
		}

		// Update unclustered point layer color
		if (map.getLayer(unclusteredLayerId) && prev.pointColor !== pointColor) {
			map.setPaintProperty(unclusteredLayerId, "circle-color", pointColor)
		}

		stylePropsRef.current = { clusterColors, clusterThresholds, pointColor }
	}, [
		isLoaded,
		map,
		clusterLayerId,
		unclusteredLayerId,
		clusterColors,
		clusterThresholds,
		pointColor
	])

	// Handle click events
	useEffect(() => {
		if (!isLoaded || !map) return

		// Cluster click handler - zoom into cluster
		const handleClusterClick = async (
			e: MapLibreGL.MapMouseEvent & {
				features?: MapLibreGL.MapGeoJSONFeature[]
			}
		) => {
			const features = map.queryRenderedFeatures(e.point, {
				layers: [clusterLayerId]
			})
			if (!features.length) return

			const feature = features[0]
			const clusterId = feature.properties?.cluster_id as number
			const pointCount = feature.properties?.point_count as number
			const coordinates = (feature.geometry as GeoJSON.Point).coordinates as [
				number,
				number
			]

			if (onClusterClick) {
				onClusterClick(clusterId, coordinates, pointCount)
			} else {
				// Default behavior: zoom to cluster expansion zoom
				const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource
				const zoom = await source.getClusterExpansionZoom(clusterId)
				map.easeTo({
					center: coordinates,
					zoom
				})
			}
		}

		// Unclustered point click handler
		const handlePointClick = (
			e: MapLibreGL.MapMouseEvent & {
				features?: MapLibreGL.MapGeoJSONFeature[]
			}
		) => {
			if (!onPointClick || !e.features?.length) return

			const feature = e.features[0]
			const coordinates = (
				feature.geometry as GeoJSON.Point
			).coordinates.slice() as [number, number]

			// Handle world copies
			while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
				coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360
			}

			onPointClick(
				feature as unknown as GeoJSON.Feature<GeoJSON.Point, P>,
				coordinates
			)
		}

		// Cursor style handlers
		const handleMouseEnterCluster = () => {
			map.getCanvas().style.cursor = "pointer"
		}
		const handleMouseLeaveCluster = () => {
			map.getCanvas().style.cursor = ""
		}
		const handleMouseEnterPoint = () => {
			if (onPointClick) {
				map.getCanvas().style.cursor = "pointer"
			}
		}
		const handleMouseLeavePoint = () => {
			map.getCanvas().style.cursor = ""
		}

		map.on("click", clusterLayerId, handleClusterClick)
		map.on("click", unclusteredLayerId, handlePointClick)
		map.on("mouseenter", clusterLayerId, handleMouseEnterCluster)
		map.on("mouseleave", clusterLayerId, handleMouseLeaveCluster)
		map.on("mouseenter", unclusteredLayerId, handleMouseEnterPoint)
		map.on("mouseleave", unclusteredLayerId, handleMouseLeavePoint)

		return () => {
			map.off("click", clusterLayerId, handleClusterClick)
			map.off("click", unclusteredLayerId, handlePointClick)
			map.off("mouseenter", clusterLayerId, handleMouseEnterCluster)
			map.off("mouseleave", clusterLayerId, handleMouseLeaveCluster)
			map.off("mouseenter", unclusteredLayerId, handleMouseEnterPoint)
			map.off("mouseleave", unclusteredLayerId, handleMouseLeavePoint)
		}
	}, [
		isLoaded,
		map,
		clusterLayerId,
		unclusteredLayerId,
		sourceId,
		onClusterClick,
		onPointClick
	])

	return null
}

export {
	Map,
	useMap,
	MapMarker,
	MarkerContent,
	MarkerPopup,
	MarkerTooltip,
	MarkerLabel,
	MapPopup,
	MapControls,
	MapRoute,
	MapClusterLayer
}

export type { MapRef, MapViewport }
