import { FolkShape } from "./folk-shape";
import { css, html } from "./tags";
import { getModuleApiBase } from "../shared/url-helpers";
import type {
	RoomState,
	ParticipantState,
	LocationState,
	PrivacySettings,
	PrecisionLevel,
	WaypointState,
} from "../modules/rmaps/components/map-sync";
import { RoomSync } from "../modules/rmaps/components/map-sync";
import { fuzzLocation, haversineDistance, formatDistance } from "../modules/rmaps/components/map-privacy";

const MAPLIBRE_CSS = "https://unpkg.com/maplibre-gl@4.1.2/dist/maplibre-gl.css";
const MAPLIBRE_JS = "https://unpkg.com/maplibre-gl@4.1.2/dist/maplibre-gl.js";

// Default tile provider (OpenStreetMap)
const DEFAULT_STYLE = {
	version: 8,
	sources: {
		osm: {
			type: "raster",
			tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
			tileSize: 256,
			attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
		},
	},
	layers: [
		{
			id: "osm",
			type: "raster",
			source: "osm",
		},
	],
};

const EMOJI_OPTIONS = ["😎", "🧑", "👩", "🧔", "👨", "🐱", "🐶", "🦊", "🐻", "🐸", "🌟", "🔥"];
const STALE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes

const styles = css`
	:host {
		background: var(--rs-bg-surface, #fff);
		color: var(--rs-text-primary, #1e293b);
		border-radius: 8px;
		box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
		min-width: 400px;
		min-height: 300px;
	}

	.header {
		display: flex;
		align-items: center;
		justify-content: space-between;
		padding: 8px 12px;
		background: #22c55e;
		color: white;
		border-radius: 8px 8px 0 0;
		font-size: 12px;
		font-weight: 600;
		cursor: move;
	}

	.header-title {
		display: flex;
		align-items: center;
		gap: 6px;
	}

	.header-actions {
		display: flex;
		gap: 4px;
	}

	.header-actions button {
		background: transparent;
		border: none;
		color: white;
		cursor: pointer;
		padding: 2px 6px;
		border-radius: 4px;
		font-size: 14px;
	}

	.header-actions button:hover {
		background: rgba(255, 255, 255, 0.2);
	}

	.status-dot {
		display: inline-block;
		width: 6px;
		height: 6px;
		border-radius: 50%;
		margin-left: 4px;
	}
	.status-dot.connected { background: #86efac; }
	.status-dot.disconnected { background: #fbbf24; }

	.map-container {
		width: 100%;
		height: calc(100% - 36px);
		border-radius: 0 0 8px 8px;
		overflow: hidden;
		position: relative;
	}

	:host([data-has-collab]) .map-container {
		height: calc(100% - 36px - 36px);
		border-radius: 0;
	}

	.map {
		width: 100%;
		height: 100%;
	}

	.loading {
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		display: flex;
		align-items: center;
		justify-content: center;
		background: var(--rs-bg-surface-sunken, #0f172a);
		color: var(--rs-text-muted, #64748b);
		font-size: 14px;
	}

	.loading.hidden {
		display: none;
	}

	.search-box {
		position: absolute;
		top: 10px;
		left: 10px;
		z-index: 10;
		display: flex;
		gap: 4px;
	}

	.search-input {
		padding: 8px 12px;
		border: none;
		border-radius: 4px;
		box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
		font-size: 13px;
		width: 200px;
		outline: none;
	}

	.search-btn {
		padding: 8px 12px;
		background: #22c55e;
		color: white;
		border: none;
		border-radius: 4px;
		cursor: pointer;
		box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
	}

	.search-btn:hover {
		background: #16a34a;
	}

	.locate-btn {
		position: absolute;
		bottom: 80px;
		right: 10px;
		z-index: 10;
		width: 32px;
		height: 32px;
		background: var(--rs-bg-surface, #1e293b);
		border: none;
		border-radius: 4px;
		cursor: pointer;
		box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
		font-size: 16px;
	}

	.locate-btn:hover {
		background: var(--rs-bg-surface-raised, #334155);
	}

	/* Override MapLibre default styles */
	.maplibregl-ctrl-attrib {
		font-size: 10px !important;
	}

	/* ── Collab toolbar ── */
	.collab-toolbar {
		display: flex;
		align-items: center;
		gap: 4px;
		padding: 4px 8px;
		background: var(--rs-bg-surface-sunken, #0f172a);
		border-top: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
		border-radius: 0 0 8px 8px;
		font-size: 12px;
		position: relative;
	}

	.collab-btn {
		display: flex;
		align-items: center;
		gap: 3px;
		padding: 4px 8px;
		background: var(--rs-bg-surface, #1e293b);
		border: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
		border-radius: 4px;
		cursor: pointer;
		font-size: 12px;
		white-space: nowrap;
	}

	.collab-btn:hover {
		background: var(--rs-bg-surface-raised, #334155);
	}

	.collab-btn.sharing {
		background: #dcfce7;
		border-color: #22c55e;
		animation: pulse-green 2s ease-in-out infinite;
	}

	@keyframes pulse-green {
		0%, 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.3); }
		50% { box-shadow: 0 0 0 4px rgba(34, 197, 94, 0); }
	}

	.collab-spacer {
		flex: 1;
	}

	/* ── Emoji picker ── */
	.emoji-picker {
		position: absolute;
		bottom: 40px;
		left: 8px;
		background: var(--rs-bg-surface, #1e293b);
		border: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
		border-radius: 8px;
		padding: 8px;
		box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
		z-index: 20;
		display: none;
	}

	.emoji-picker.open {
		display: block;
	}

	.emoji-picker-grid {
		display: grid;
		grid-template-columns: repeat(4, 1fr);
		gap: 4px;
	}

	.emoji-opt {
		width: 32px;
		height: 32px;
		display: flex;
		align-items: center;
		justify-content: center;
		border: 1px solid transparent;
		border-radius: 4px;
		cursor: pointer;
		font-size: 18px;
		background: none;
		padding: 0;
	}

	.emoji-opt:hover {
		background: var(--rs-bg-hover, rgba(255, 255, 255, 0.05));
		border-color: var(--rs-border, rgba(255, 255, 255, 0.1));
	}

	.emoji-opt.selected {
		border-color: #22c55e;
		background: #dcfce7;
	}

	/* ── Participant panel ── */
	.participant-panel {
		position: absolute;
		top: 0;
		right: 0;
		width: 200px;
		height: 100%;
		background: var(--rs-bg-surface, #1e293b);
		border-left: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
		z-index: 15;
		overflow-y: auto;
		display: none;
		flex-direction: column;
	}

	.participant-panel.open {
		display: flex;
	}

	.participant-panel-header {
		display: flex;
		align-items: center;
		justify-content: space-between;
		padding: 8px 10px;
		border-bottom: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
		font-size: 12px;
		font-weight: 600;
		color: var(--rs-text-secondary, #94a3b8);
	}

	.participant-panel-close {
		background: none;
		border: none;
		cursor: pointer;
		font-size: 14px;
		color: var(--rs-text-secondary, #94a3b8);
		padding: 0 2px;
	}

	.participant-entry {
		display: flex;
		align-items: center;
		gap: 8px;
		padding: 6px 10px;
		border-bottom: 1px solid var(--rs-border-subtle, rgba(255, 255, 255, 0.06));
		font-size: 11px;
	}

	.participant-emoji {
		font-size: 18px;
		flex-shrink: 0;
	}

	.participant-info {
		flex: 1;
		min-width: 0;
	}

	.participant-name {
		font-weight: 500;
		color: var(--rs-text-primary, #e2e8f0);
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
	}

	.participant-meta {
		color: var(--rs-text-secondary, #94a3b8);
		font-size: 10px;
	}

	.participant-status-dot {
		width: 6px;
		height: 6px;
		border-radius: 50%;
		flex-shrink: 0;
	}

	.participant-status-dot.online { background: #22c55e; }
	.participant-status-dot.away { background: #fbbf24; }
	.participant-status-dot.ghost { background: #94a3b8; }
	.participant-status-dot.offline { background: var(--rs-text-muted, #64748b); }

	/* ── Waypoint input ── */
	.waypoint-input-wrap {
		position: absolute;
		top: 50%;
		left: 50%;
		transform: translate(-50%, -50%);
		z-index: 20;
		background: var(--rs-bg-surface, #1e293b);
		border-radius: 8px;
		padding: 12px;
		box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
		display: none;
	}

	.waypoint-input-wrap.open {
		display: flex;
		gap: 6px;
	}

	.waypoint-input {
		padding: 6px 10px;
		border: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
		border-radius: 4px;
		font-size: 12px;
		outline: none;
		width: 140px;
	}

	.waypoint-input:focus {
		border-color: #22c55e;
	}

	.waypoint-submit {
		padding: 6px 10px;
		background: #22c55e;
		color: white;
		border: none;
		border-radius: 4px;
		cursor: pointer;
		font-size: 12px;
	}

	.waypoint-cancel {
		padding: 6px 10px;
		background: var(--rs-bg-surface-raised, #334155);
		color: var(--rs-text-muted, #64748b);
		border: none;
		border-radius: 4px;
		cursor: pointer;
		font-size: 12px;
	}
`;

export interface MapMarker {
	id: string;
	lng: number;
	lat: number;
	color?: string;
	label?: string;
}

// MapLibre types (loaded dynamically from CDN)
interface MapLibreMap {
	flyTo(options: { center: [number, number] }): void;
	setZoom(zoom: number): void;
	getCenter(): { lng: number; lat: number };
	getZoom(): number;
	addControl(control: unknown, position?: string): void;
	on(event: string, handler: (e: MapLibreEvent) => void): void;
	remove(): void;
}

interface MapLibreEvent {
	lngLat: { lng: number; lat: number };
}

interface MapLibreMarker {
	setLngLat(coords: [number, number]): this;
	addTo(map: MapLibreMap): this;
	setPopup(popup: unknown): this;
	remove(): void;
	getElement(): HTMLElement;
}

interface MapLibreGL {
	Map: new (options: {
		container: HTMLElement;
		style: object;
		center: [number, number];
		zoom: number;
	}) => MapLibreMap;
	NavigationControl: new () => unknown;
	Marker: new (options?: { element?: HTMLElement }) => MapLibreMarker;
	Popup: new (options?: { offset?: number }) => { setText(text: string): unknown; setHTML(html: string): unknown };
}

declare global {
	interface HTMLElementTagNameMap {
		"folk-map": FolkMap;
	}
	interface Window {
		maplibregl: MapLibreGL;
	}
}

export class FolkMap extends FolkShape {
	static override tagName = "folk-map";

	static {
		const sheet = new CSSStyleSheet();
		const parentRules = Array.from(FolkShape.styles.cssRules)
			.map((r) => r.cssText)
			.join("\n");
		const childRules = Array.from(styles.cssRules)
			.map((r) => r.cssText)
			.join("\n");
		sheet.replaceSync(`${parentRules}\n${childRules}`);
		this.styles = sheet;
	}

	// ── Existing solo-map state ──
	#map: MapLibreMap | null = null;
	#markers: MapMarker[] = [];
	#mapMarkerInstances = new Map<string, MapLibreMarker>();
	#center: [number, number] = [-74.006, 40.7128]; // NYC default
	#zoom = 12;
	#mapEl: HTMLElement | null = null;
	#loadingEl: HTMLElement | null = null;

	// ── Collab state ──
	#roomSlug = "";
	#sync: RoomSync | null = null;
	#syncUrl = "";
	#participantId = "";
	#userName = "";
	#userEmoji = "😎";
	#userColor = "#22c55e";
	#sharingLocation = false;
	#watchId: number | null = null;
	#privacySettings: PrivacySettings = { precision: "exact" as PrecisionLevel, ghostMode: false };
	#participantMarkers = new Map<string, MapLibreMarker>();
	#waypointMarkers = new Map<string, MapLibreMarker>();
	#syncConnected = false;
	#stalenessTimer: ReturnType<typeof setInterval> | null = null;
	#showParticipants = false;
	#showEmojiPicker = false;

	// ── DOM refs for collab UI ──
	#collabToolbar: HTMLElement | null = null;
	#emojiPickerEl: HTMLElement | null = null;
	#participantPanel: HTMLElement | null = null;
	#participantList: HTMLElement | null = null;
	#shareBtn: HTMLButtonElement | null = null;
	#countBadge: HTMLElement | null = null;
	#statusDot: HTMLElement | null = null;
	#headerLabel: HTMLElement | null = null;
	#waypointInputWrap: HTMLElement | null = null;
	#emojiBtnEl: HTMLButtonElement | null = null;

	get center(): [number, number] {
		return this.#center;
	}

	set center(value: [number, number]) {
		this.#center = value;
		this.#map?.flyTo({ center: value });
	}

	get zoom(): number {
		return this.#zoom;
	}

	set zoom(value: number) {
		this.#zoom = value;
		this.#map?.setZoom(value);
	}

	get markers(): MapMarker[] {
		return this.#markers;
	}

	get roomSlug(): string {
		return this.#roomSlug;
	}

	set roomSlug(value: string) {
		const slug = (value || "").trim().toLowerCase().replace(/[^a-z0-9-]/g, "-");
		if (slug === this.#roomSlug) return;
		if (this.#roomSlug) this.#leaveRoom();
		this.#roomSlug = slug;
		if (slug) {
			this.setAttribute("data-has-collab", "");
			if (this.#collabToolbar) this.#collabToolbar.style.display = "flex";
			this.#initRoomSync();
		} else {
			this.removeAttribute("data-has-collab");
			if (this.#collabToolbar) this.#collabToolbar.style.display = "none";
		}
		if (this.#headerLabel) {
			this.#headerLabel.textContent = slug ? `Map — ${slug}` : "Map";
		}
	}

	addMarker(marker: MapMarker) {
		this.#markers.push(marker);
		this.#renderMarker(marker);
		this.dispatchEvent(new CustomEvent("marker-add", { detail: { marker } }));
	}

	removeMarker(id: string) {
		const instance = this.#mapMarkerInstances.get(id);
		if (instance) {
			instance.remove();
			this.#mapMarkerInstances.delete(id);
		}
		this.#markers = this.#markers.filter((m) => m.id !== id);
	}

	override createRenderRoot() {
		const root = super.createRenderRoot();

		// Parse initial attributes
		const centerAttr = this.getAttribute("center");
		if (centerAttr) {
			const [lng, lat] = centerAttr.split(",").map(Number);
			if (!isNaN(lng) && !isNaN(lat)) {
				this.#center = [lng, lat];
			}
		}
		const zoomAttr = this.getAttribute("zoom");
		if (zoomAttr && !isNaN(Number(zoomAttr))) {
			this.#zoom = Number(zoomAttr);
		}

		const wrapper = document.createElement("div");
		wrapper.innerHTML = html`
			<div class="header">
				<span class="header-title">
					<span>🗺</span>
					<span class="header-label">Map</span>
					<span class="status-dot disconnected" style="display:none"></span>
				</span>
				<div class="header-actions">
					<button class="close-btn" title="Close">×</button>
				</div>
			</div>
			<div class="map-container">
				<div class="loading">Loading map...</div>
				<div class="search-box">
					<input type="text" class="search-input" placeholder="Search location..." />
					<button class="search-btn">🔍</button>
				</div>
				<button class="locate-btn" title="My Location">📍</button>
				<div class="map"></div>
				<!-- Participant panel overlay -->
				<div class="participant-panel">
					<div class="participant-panel-header">
						<span>Participants</span>
						<button class="participant-panel-close">×</button>
					</div>
					<div class="participant-list"></div>
				</div>
				<!-- Waypoint name input -->
				<div class="waypoint-input-wrap">
					<input type="text" class="waypoint-input" placeholder="Pin name..." />
					<button class="waypoint-submit">📌</button>
					<button class="waypoint-cancel">×</button>
				</div>
			</div>
			<!-- Collab toolbar (hidden until roomSlug set) -->
			<div class="collab-toolbar" style="display:none">
				<button class="collab-btn emoji-btn" title="Change avatar">😎</button>
				<button class="collab-btn share-btn" title="Share location">📍 Share</button>
				<button class="collab-btn pin-btn" title="Drop pin">📌 Pin</button>
				<div class="collab-spacer"></div>
				<button class="collab-btn count-btn" title="Participants">👥 <span class="count-badge">0</span></button>
			</div>
			<!-- Emoji picker dropdown -->
			<div class="emoji-picker">
				<div class="emoji-picker-grid">
					${EMOJI_OPTIONS.map((e) => `<button class="emoji-opt" data-emoji="${e}">${e}</button>`).join("")}
				</div>
			</div>
		`;

		// Replace the container div (slot's parent) with our wrapper
		const slot = root.querySelector("slot");
		const containerDiv = slot?.parentElement as HTMLElement;
		if (containerDiv) {
			containerDiv.replaceWith(wrapper);
		}

		// ── Grab DOM refs ──
		this.#mapEl = wrapper.querySelector(".map");
		this.#loadingEl = wrapper.querySelector(".loading");
		this.#headerLabel = wrapper.querySelector(".header-label");
		this.#statusDot = wrapper.querySelector(".status-dot");
		this.#collabToolbar = wrapper.querySelector(".collab-toolbar");
		this.#emojiPickerEl = wrapper.querySelector(".emoji-picker");
		this.#participantPanel = wrapper.querySelector(".participant-panel");
		this.#participantList = wrapper.querySelector(".participant-list");
		this.#waypointInputWrap = wrapper.querySelector(".waypoint-input-wrap");

		const searchInput = wrapper.querySelector(".search-input") as HTMLInputElement;
		const searchBtn = wrapper.querySelector(".search-btn") as HTMLButtonElement;
		const locateBtn = wrapper.querySelector(".locate-btn") as HTMLButtonElement;
		const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;

		this.#shareBtn = wrapper.querySelector(".share-btn") as HTMLButtonElement;
		this.#emojiBtnEl = wrapper.querySelector(".emoji-btn") as HTMLButtonElement;
		this.#countBadge = wrapper.querySelector(".count-badge") as HTMLElement;
		const pinBtn = wrapper.querySelector(".pin-btn") as HTMLButtonElement;
		const countBtn = wrapper.querySelector(".count-btn") as HTMLButtonElement;
		const panelCloseBtn = wrapper.querySelector(".participant-panel-close") as HTMLButtonElement;
		const waypointInput = wrapper.querySelector(".waypoint-input") as HTMLInputElement;
		const waypointSubmit = wrapper.querySelector(".waypoint-submit") as HTMLButtonElement;
		const waypointCancel = wrapper.querySelector(".waypoint-cancel") as HTMLButtonElement;

		// Load MapLibre and initialize map
		this.#loadMapLibre().then(() => {
			this.#initMap();
		});

		// ── Search handler ──
		const handleSearch = async () => {
			const query = searchInput.value.trim();
			if (!query) return;
			try {
				const response = await fetch(
					`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}`
				);
				const results = await response.json();
				if (results.length > 0) {
					const { lon, lat } = results[0];
					this.center = [parseFloat(lon), parseFloat(lat)];
					this.zoom = 15;
				}
			} catch (error) {
				console.error("Search error:", error);
			}
		};

		searchBtn.addEventListener("click", (e) => {
			e.stopPropagation();
			handleSearch();
		});

		searchInput.addEventListener("keydown", (e) => {
			if (e.key === "Enter") {
				e.preventDefault();
				handleSearch();
			}
		});

		// Prevent map interactions from triggering shape drag
		searchInput.addEventListener("pointerdown", (e) => e.stopPropagation());

		// Location button (solo locate — pan to my location)
		locateBtn.addEventListener("click", (e) => {
			e.stopPropagation();
			if ("geolocation" in navigator) {
				navigator.geolocation.getCurrentPosition(
					(pos) => {
						this.center = [pos.coords.longitude, pos.coords.latitude];
						this.zoom = 15;
					},
					(err) => {
						console.error("Geolocation error:", err);
					}
				);
			}
		});

		// Close button
		closeBtn.addEventListener("click", (e) => {
			e.stopPropagation();
			this.dispatchEvent(new CustomEvent("close"));
		});

		// ── Collab button handlers ──
		// Emoji button → toggle picker
		this.#emojiBtnEl.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#showEmojiPicker = !this.#showEmojiPicker;
			this.#emojiPickerEl?.classList.toggle("open", this.#showEmojiPicker);
		});

		// Emoji option clicks
		this.#emojiPickerEl?.addEventListener("click", (e) => {
			e.stopPropagation();
			const target = (e.target as HTMLElement).closest(".emoji-opt") as HTMLElement | null;
			if (!target) return;
			const emoji = target.dataset.emoji;
			if (emoji) this.#changeEmoji(emoji);
		});

		// Share location toggle
		this.#shareBtn.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#toggleLocationSharing();
		});

		// Drop waypoint
		pinBtn.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#showWaypointInput();
		});

		// Waypoint input events
		waypointInput.addEventListener("pointerdown", (e) => e.stopPropagation());
		waypointSubmit.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#submitWaypoint(waypointInput.value.trim());
			waypointInput.value = "";
		});
		waypointCancel.addEventListener("click", (e) => {
			e.stopPropagation();
			waypointInput.value = "";
			this.#waypointInputWrap?.classList.remove("open");
		});
		waypointInput.addEventListener("keydown", (e) => {
			if (e.key === "Enter") {
				e.preventDefault();
				this.#submitWaypoint(waypointInput.value.trim());
				waypointInput.value = "";
			} else if (e.key === "Escape") {
				waypointInput.value = "";
				this.#waypointInputWrap?.classList.remove("open");
			}
		});

		// Participant count → toggle panel
		countBtn.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#showParticipants = !this.#showParticipants;
			this.#participantPanel?.classList.toggle("open", this.#showParticipants);
		});

		panelCloseBtn.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#showParticipants = false;
			this.#participantPanel?.classList.remove("open");
		});

		// Stop propagation on collab toolbar
		this.#collabToolbar?.addEventListener("pointerdown", (e) => e.stopPropagation());
		this.#emojiPickerEl?.addEventListener("pointerdown", (e) => e.stopPropagation());

		return root;
	}

	disconnectedCallback() {
		super.disconnectedCallback?.();
		this.#leaveRoom();
		this.#map?.remove();
		this.#map = null;
	}

	// ── MapLibre loading ──

	async #loadMapLibre() {
		if (window.maplibregl) return;

		const link = document.createElement("link");
		link.rel = "stylesheet";
		link.href = MAPLIBRE_CSS;
		document.head.appendChild(link);

		return new Promise<void>((resolve, reject) => {
			const script = document.createElement("script");
			script.src = MAPLIBRE_JS;
			script.onload = () => resolve();
			script.onerror = reject;
			document.head.appendChild(script);
		});
	}

	#initMap() {
		if (!this.#mapEl || !window.maplibregl) return;

		this.#map = new window.maplibregl.Map({
			container: this.#mapEl,
			style: DEFAULT_STYLE,
			center: this.#center,
			zoom: this.#zoom,
		});

		this.#map.addControl(new window.maplibregl.NavigationControl(), "top-right");

		this.#map.on("load", () => {
			if (this.#loadingEl) {
				this.#loadingEl.classList.add("hidden");
			}
			this.#markers.forEach((marker) => this.#renderMarker(marker));
		});

		this.#map.on("moveend", () => {
			const center = this.#map!.getCenter();
			this.#center = [center.lng, center.lat];
			this.#zoom = this.#map!.getZoom();
			this.dispatchEvent(
				new CustomEvent("map-move", {
					detail: { center: this.#center, zoom: this.#zoom },
				})
			);
		});

		// Click to add marker (solo mode only)
		this.#map.on("click", (e) => {
			if (this.#roomSlug) return; // In collab mode, don't add solo markers on click
			const marker: MapMarker = {
				id: crypto.randomUUID(),
				lng: e.lngLat.lng,
				lat: e.lngLat.lat,
				color: "#22c55e",
			};
			this.addMarker(marker);
		});
	}

	#renderMarker(marker: MapMarker) {
		if (!this.#map || !window.maplibregl) return;

		const el = document.createElement("div");
		el.style.cssText = `
			width: 24px;
			height: 24px;
			background: ${marker.color || "#22c55e"};
			border: 2px solid white;
			border-radius: 50%;
			cursor: pointer;
			box-shadow: 0 2px 6px rgba(0,0,0,0.3);
		`;

		const mapMarker = new window.maplibregl.Marker({ element: el })
			.setLngLat([marker.lng, marker.lat])
			.addTo(this.#map);

		if (marker.label) {
			mapMarker.setPopup(
				new window.maplibregl.Popup({ offset: 25 }).setText(marker.label)
			);
		}

		this.#mapMarkerInstances.set(marker.id, mapMarker);
	}

	// ── Room sync methods ──

	#loadUserProfile() {
		try {
			const stored = localStorage.getItem("rmaps_user");
			if (stored) {
				const profile = JSON.parse(stored);
				this.#userName = profile.name || "";
				this.#userEmoji = profile.emoji || "😎";
				this.#userColor = profile.color || "#22c55e";
				if (profile.privacy) {
					this.#privacySettings = profile.privacy;
				}
			}
		} catch {}
		// Generate participant ID if not set
		if (!this.#participantId) {
			this.#participantId = localStorage.getItem("rmaps_participant_id") || crypto.randomUUID();
			localStorage.setItem("rmaps_participant_id", this.#participantId);
		}
	}

	#saveUserProfile() {
		try {
			localStorage.setItem("rmaps_user", JSON.stringify({
				name: this.#userName,
				emoji: this.#userEmoji,
				color: this.#userColor,
				privacy: this.#privacySettings,
			}));
		} catch {}
	}

	#getApiBase(): string {
		return getModuleApiBase("rmaps");
	}

	async #initRoomSync() {
		if (!this.#roomSlug) return;

		this.#loadUserProfile();

		// Prompt for name if not set
		if (!this.#userName) {
			this.#userName = prompt("Your name for this map room:") || "Anonymous";
			this.#saveUserProfile();
		}

		// Fetch sync URL from API
		try {
			const apiBase = this.#getApiBase();
			const resp = await fetch(`${apiBase}/api/sync-url`);
			if (resp.ok) {
				const data = await resp.json();
				this.#syncUrl = data.url || "";
			}
		} catch {
			console.warn("[folk-map] Could not fetch sync URL, running local-only");
		}

		// Create RoomSync
		this.#sync = new RoomSync(
			this.#roomSlug,
			this.#participantId,
			(state) => this.#onRoomStateChange(state),
			(connected) => {
				this.#syncConnected = connected;
				if (this.#statusDot) {
					this.#statusDot.style.display = "";
					this.#statusDot.className = `status-dot ${connected ? "connected" : "disconnected"}`;
				}
			}
		);

		// Connect (falls back to local-only if no syncUrl)
		this.#sync.connect(this.#syncUrl || undefined);

		// Join with participant info
		const participant: ParticipantState = {
			id: this.#participantId,
			name: this.#userName,
			emoji: this.#userEmoji,
			color: this.#userColor,
			joinedAt: new Date().toISOString(),
			lastSeen: new Date().toISOString(),
			status: "online",
		};
		this.#sync.join(participant);

		// Update emoji button
		if (this.#emojiBtnEl) this.#emojiBtnEl.textContent = this.#userEmoji;

		// Start staleness timer
		this.#stalenessTimer = setInterval(() => this.#refreshStaleness(), 15000);
	}

	#leaveRoom() {
		// Stop GPS watching
		if (this.#watchId !== null) {
			navigator.geolocation.clearWatch(this.#watchId);
			this.#watchId = null;
		}
		this.#sharingLocation = false;
		if (this.#shareBtn) this.#shareBtn.classList.remove("sharing");

		// Leave sync
		this.#sync?.leave();
		this.#sync = null;

		// Clear participant markers
		for (const m of this.#participantMarkers.values()) m.remove();
		this.#participantMarkers.clear();

		// Clear waypoint markers
		for (const m of this.#waypointMarkers.values()) m.remove();
		this.#waypointMarkers.clear();

		// Clear staleness timer
		if (this.#stalenessTimer) {
			clearInterval(this.#stalenessTimer);
			this.#stalenessTimer = null;
		}

		// Reset status dot
		if (this.#statusDot) this.#statusDot.style.display = "none";

		// Close panels
		this.#showParticipants = false;
		this.#showEmojiPicker = false;
		this.#participantPanel?.classList.remove("open");
		this.#emojiPickerEl?.classList.remove("open");
	}

	#onRoomStateChange(state: RoomState) {
		if (!this.#map || !window.maplibregl) return;

		const participants = Object.values(state.participants);
		const now = Date.now();

		// ── Update/create participant markers ──
		const activeIds = new Set<string>();
		for (const p of participants) {
			if (p.id === this.#participantId && !this.#sharingLocation) continue;
			activeIds.add(p.id);

			if (!p.location) {
				// No location — remove marker if exists
				const existing = this.#participantMarkers.get(p.id);
				if (existing) {
					existing.remove();
					this.#participantMarkers.delete(p.id);
				}
				continue;
			}

			const age = now - new Date(p.lastSeen).getTime();
			const isStale = age > STALE_THRESHOLD_MS;
			const opacity = isStale ? 0.4 : 1;
			const ageText = isStale ? ` (${formatDistance(0)}${Math.floor(age / 60000)}m ago)` : "";

			const existing = this.#participantMarkers.get(p.id);
			if (existing) {
				// Update position
				existing.setLngLat([p.location.longitude, p.location.latitude]);
				const el = existing.getElement();
				el.style.opacity = String(opacity);
				// Update tooltip
				const nameLabel = el.querySelector(".p-label") as HTMLElement;
				if (nameLabel) nameLabel.textContent = p.name + ageText;
				// Update heading arrow
				const arrow = el.querySelector(".heading-arrow") as HTMLElement;
				if (arrow && p.location.heading != null) {
					arrow.style.display = "";
					arrow.style.transform = `rotate(${p.location.heading}deg)`;
				} else if (arrow) {
					arrow.style.display = "none";
				}
			} else {
				// Create new participant marker
				const el = document.createElement("div");
				el.style.cssText = `
					display: flex;
					flex-direction: column;
					align-items: center;
					cursor: pointer;
					opacity: ${opacity};
					pointer-events: auto;
				`;
				el.innerHTML = `
					<div class="heading-arrow" style="
						width: 0; height: 0;
						border-left: 5px solid transparent;
						border-right: 5px solid transparent;
						border-bottom: 8px solid ${p.color || "#22c55e"};
						margin-bottom: 2px;
						display: ${p.location.heading != null ? "" : "none"};
						transform: rotate(${p.location.heading || 0}deg);
					"></div>
					<div style="
						font-size: 22px;
						line-height: 1;
						text-shadow: 0 1px 3px rgba(0,0,0,0.3);
					">${p.emoji || "😎"}</div>
					<div class="p-label" style="
						font-size: 9px;
						background: rgba(0,0,0,0.6);
						color: white;
						padding: 1px 4px;
						border-radius: 3px;
						white-space: nowrap;
						margin-top: 2px;
					">${p.name}${ageText}</div>
				`;

				const marker = new window.maplibregl.Marker({ element: el })
					.setLngLat([p.location.longitude, p.location.latitude])
					.addTo(this.#map);
				this.#participantMarkers.set(p.id, marker);
			}
		}

		// Remove markers for participants who left
		for (const [id, marker] of this.#participantMarkers) {
			if (!activeIds.has(id)) {
				marker.remove();
				this.#participantMarkers.delete(id);
			}
		}

		// ── Update waypoint markers ──
		const activeWpIds = new Set<string>();
		for (const wp of state.waypoints) {
			activeWpIds.add(wp.id);
			const existing = this.#waypointMarkers.get(wp.id);
			if (!existing) {
				const el = document.createElement("div");
				el.style.cssText = `
					display: flex;
					flex-direction: column;
					align-items: center;
					cursor: pointer;
				`;
				el.innerHTML = `
					<div style="font-size: 20px; line-height: 1;">📌</div>
					<div style="
						font-size: 9px;
						background: rgba(0,0,0,0.6);
						color: white;
						padding: 1px 4px;
						border-radius: 3px;
						white-space: nowrap;
						margin-top: 2px;
					">${wp.name || "Pin"}</div>
				`;

				const marker = new window.maplibregl.Marker({ element: el })
					.setLngLat([wp.longitude, wp.latitude])
					.addTo(this.#map);
				this.#waypointMarkers.set(wp.id, marker);
			}
		}

		// Remove deleted waypoints
		for (const [id, marker] of this.#waypointMarkers) {
			if (!activeWpIds.has(id)) {
				marker.remove();
				this.#waypointMarkers.delete(id);
			}
		}

		// ── Update count badge ──
		if (this.#countBadge) {
			this.#countBadge.textContent = String(participants.length);
		}

		// ── Refresh participant panel if open ──
		if (this.#showParticipants) {
			this.#renderParticipantPanel(state);
		}
	}

	#toggleLocationSharing() {
		if (!this.#sync) return;

		if (this.#sharingLocation) {
			// Stop sharing
			if (this.#watchId !== null) {
				navigator.geolocation.clearWatch(this.#watchId);
				this.#watchId = null;
			}
			this.#sharingLocation = false;
			this.#shareBtn?.classList.remove("sharing");
			if (this.#shareBtn) this.#shareBtn.innerHTML = "📍 Share";
			this.#sync.clearLocation();
			return;
		}

		if (!("geolocation" in navigator)) return;

		this.#sharingLocation = true;
		this.#shareBtn?.classList.add("sharing");
		if (this.#shareBtn) this.#shareBtn.innerHTML = "📍 Sharing...";

		this.#watchId = navigator.geolocation.watchPosition(
			(pos) => {
				const { latitude, longitude, accuracy, altitude, heading, speed } = pos.coords;
				const fuzzed = fuzzLocation(latitude, longitude, this.#privacySettings.precision);

				const location: LocationState = {
					latitude: fuzzed.latitude,
					longitude: fuzzed.longitude,
					accuracy,
					altitude: altitude ?? undefined,
					heading: heading ?? undefined,
					speed: speed ?? undefined,
					timestamp: new Date().toISOString(),
					source: "gps",
				};
				this.#sync?.updateLocation(location);
			},
			(err) => {
				console.warn("[folk-map] GPS error:", err);
				// Try lower accuracy
				if (err.code === err.TIMEOUT && this.#watchId !== null) {
					navigator.geolocation.clearWatch(this.#watchId);
					this.#watchId = navigator.geolocation.watchPosition(
						(pos) => {
							const { latitude, longitude, accuracy } = pos.coords;
							const fuzzed = fuzzLocation(latitude, longitude, this.#privacySettings.precision);
							this.#sync?.updateLocation({
								latitude: fuzzed.latitude,
								longitude: fuzzed.longitude,
								accuracy,
								timestamp: new Date().toISOString(),
								source: "network",
							});
						},
						() => {},
						{ enableHighAccuracy: false, timeout: 30000, maximumAge: 60000 }
					);
				}
			},
			{ enableHighAccuracy: true, timeout: 10000, maximumAge: 5000 }
		);
	}

	#showWaypointInput() {
		this.#waypointInputWrap?.classList.add("open");
		const input = this.#waypointInputWrap?.querySelector(".waypoint-input") as HTMLInputElement;
		if (input) {
			input.value = "";
			input.focus();
		}
	}

	#submitWaypoint(name: string) {
		this.#waypointInputWrap?.classList.remove("open");
		if (!this.#sync || !this.#map) return;

		const center = this.#map.getCenter();
		const waypoint: WaypointState = {
			id: crypto.randomUUID(),
			name: name || "Pin",
			latitude: center.lat,
			longitude: center.lng,
			createdBy: this.#participantId,
			createdAt: new Date().toISOString(),
			type: "poi",
		};
		this.#sync.addWaypoint(waypoint);
	}

	#changeEmoji(emoji: string) {
		this.#userEmoji = emoji;
		this.#saveUserProfile();
		if (this.#emojiBtnEl) this.#emojiBtnEl.textContent = emoji;

		// Close picker
		this.#showEmojiPicker = false;
		this.#emojiPickerEl?.classList.remove("open");

		// Update selected state in picker
		this.#emojiPickerEl?.querySelectorAll(".emoji-opt").forEach((el) => {
			el.classList.toggle("selected", (el as HTMLElement).dataset.emoji === emoji);
		});

		// Re-join with updated emoji
		if (this.#sync) {
			this.#sync.join({
				id: this.#participantId,
				name: this.#userName,
				emoji: this.#userEmoji,
				color: this.#userColor,
				joinedAt: new Date().toISOString(),
				lastSeen: new Date().toISOString(),
				status: "online",
			});
		}
	}

	#refreshStaleness() {
		if (!this.#sync) return;
		const state = this.#sync.getState();
		const now = Date.now();

		for (const p of Object.values(state.participants)) {
			if (p.id === this.#participantId) continue;
			const marker = this.#participantMarkers.get(p.id);
			if (!marker) continue;

			const age = now - new Date(p.lastSeen).getTime();
			const isStale = age > STALE_THRESHOLD_MS;
			const el = marker.getElement();
			el.style.opacity = isStale ? "0.4" : "1";

			const nameLabel = el.querySelector(".p-label") as HTMLElement;
			if (nameLabel) {
				const ageText = isStale ? ` (${Math.floor(age / 60000)}m ago)` : "";
				nameLabel.textContent = p.name + ageText;
			}
		}
	}

	#renderParticipantPanel(state: RoomState) {
		if (!this.#participantList) return;

		const myLocation = state.participants[this.#participantId]?.location;
		const entries = Object.values(state.participants).map((p) => {
			let distText = "";
			if (myLocation && p.location && p.id !== this.#participantId) {
				const dist = haversineDistance(
					myLocation.latitude, myLocation.longitude,
					p.location.latitude, p.location.longitude
				);
				distText = formatDistance(dist);
			}

			const now = Date.now();
			const age = now - new Date(p.lastSeen).getTime();
			const isStale = age > STALE_THRESHOLD_MS;
			const statusClass = isStale ? "away" : p.status;

			return `
				<div class="participant-entry">
					<span class="participant-emoji">${p.emoji || "😎"}</span>
					<div class="participant-info">
						<div class="participant-name">${p.name}${p.id === this.#participantId ? " (you)" : ""}</div>
						<div class="participant-meta">
							${p.location ? (distText || "sharing") : "no location"}
							${isStale ? ` · ${Math.floor(age / 60000)}m ago` : ""}
						</div>
					</div>
					<div class="participant-status-dot ${statusClass}"></div>
				</div>
			`;
		});

		this.#participantList.innerHTML = entries.join("");
	}

	// ── Serialization ──

	static override fromData(data: Record<string, any>): FolkMap {
		const shape = FolkShape.fromData(data) as FolkMap;
		if (data.center) shape.center = data.center;
		if (data.zoom !== undefined) shape.zoom = data.zoom;
		if (data.roomSlug) shape.roomSlug = data.roomSlug;
		return shape;
	}

	override toJSON() {
		return {
			...super.toJSON(),
			type: "folk-map",
			center: this.center,
			zoom: this.zoom,
			markers: this.markers,
			roomSlug: this.#roomSlug,
		};
	}

	override applyData(data: Record<string, any>): void {
		super.applyData(data);
		if (data.center !== undefined) this.center = data.center;
		if (data.zoom !== undefined && this.zoom !== data.zoom) this.zoom = data.zoom;
		if ("roomSlug" in data && this.#roomSlug !== data.roomSlug) {
			this.roomSlug = data.roomSlug;
		}
	}
}
