import React from 'react';
import { createRoot, type Root } from 'react-dom/client';
import Box from '@mui/material/Box';
import type { SxProps, Theme } from '@mui/material/styles';
import {
  APIProvider,
  Map as GoogleMap,
  AdvancedMarker,
  useMap,
  useMapsLibrary,
} from '@vis.gl/react-google-maps';
import { MarkerClusterer, GridAlgorithm } from '@googlemaps/markerclusterer';
import { MapPin } from '../../atoms/MapPin';
import { ClusterMarker } from '../../atoms/ClusterMarker';
import { MapPopup } from '../../molecules/MapPopup';
import { ClusterPopup } from '../../molecules/ClusterPopup';
import { Typography } from '../../atoms/Typography';
import type { ProviderData } from '../../pages/ProvidersStep';

// ─── Constants ──────────────────────────────────────────────────────────────

/** Sydney — fallback centre when no providers have coords and no default supplied */
const FALLBACK_CENTER = { lat: -33.8688, lng: 151.2093 };
const FALLBACK_ZOOM = 5;
/** Google Maps requires a mapId for AdvancedMarker support */
const MAP_ID = 'fa-provider-map';
/** fitBounds padding (applied as google.maps.Padding) */
const BOUNDS_PADDING = { top: 64, right: 48, bottom: 64, left: 48 };
/** Screen-pixel radius at which nearby pins collapse into a cluster */
const CLUSTER_GRID_SIZE = 70;
/** Zoom level above which clustering is disabled (pins show individually) */
const CLUSTER_MAX_ZOOM = 13;
/** Zoom level the map animates to on cluster drill-in (street-level, past
 *  CLUSTER_MAX_ZOOM so nearby cluster members break apart into their own pins) */
const DRILL_IN_ZOOM = 15;
/** Exit-animation duration for popups on close — keep in sync with the
 *  transition values set on MapPopup/ClusterPopup. */
const POPUP_EXIT_MS = 180;

// ─── Types ──────────────────────────────────────────────────────────────────

/** Shape of the currently-active provider or cluster selection, emitted to
 *  callers that opt into external popup rendering (see `externalisePopups`). */
export interface ProviderMapActiveState {
  /** Active single provider, if a pin was tapped (or a cluster row drilled into) */
  provider: ProviderData | null;
  /** Active cluster, if a cluster marker was tapped and no row has been drilled into */
  cluster: { providers: ProviderData[]; position: { lat: number; lng: number } } | null;
  /** True while the exit animation is running — callers may want to mirror it */
  exiting: boolean;
}

/** Imperative handle exposed via ref. Used when rendering popups externally. */
export interface ProviderMapHandle {
  /** Close the currently-active popup (animated). No-op if nothing is open. */
  clearActive: () => void;
  /** Pan + zoom the map to a provider's coords and set them as the active
   *  single-provider selection. Equivalent to a cluster-row tap. */
  drillIntoProvider: (id: string) => void;
}

/** Props for the FA ProviderMap organism */
export interface ProviderMapProps {
  /** Providers to render as pins. Providers without coords are filtered out silently. */
  providers: ProviderData[];
  /** ID of the provider whose popup should open (external selection, e.g. list hover) */
  selectedProviderId?: string | null;
  /** Called when the user clicks through a popup — usually triggers navigation */
  onSelectProvider: (id: string) => void;
  /** Initial map centre — used only when no providers have coords */
  defaultCenter?: { lat: number; lng: number };
  /** Initial zoom — used only when no providers have coords */
  defaultZoom?: number;
  /** Google Maps API key. Defaults to `import.meta.env.VITE_GOOGLE_MAPS_API_KEY`. */
  apiKey?: string;
  /** When true, suppress the organism's own MapPopup + ClusterPopup rendering.
   *  The active state is still tracked internally (pins still hide when active)
   *  and emitted via `onActiveChange` so callers can render a drawer, sheet,
   *  or other external container. Used by the mobile map-first layout. */
  externalisePopups?: boolean;
  /** Fires whenever the active provider/cluster state changes. Paired with
   *  `externalisePopups` — the caller uses this to drive external UI. */
  onActiveChange?: (state: ProviderMapActiveState) => void;
  /** MUI sx prop for the root element */
  sx?: SxProps<Theme>;
}

interface ActiveCluster {
  providers: ProviderData[];
  position: google.maps.LatLngLiteral;
}

// ─── Internal components ────────────────────────────────────────────────────

/**
 * Fits the map to the bounds of all providers with coords. Runs whenever the
 * provider list changes. Sited inside APIProvider so `useMap()` resolves.
 */
const FitBounds: React.FC<{ providers: ProviderData[] }> = ({ providers }) => {
  const map = useMap();
  React.useEffect(() => {
    if (!map) return;
    const withCoords = providers.filter((p) => p.coords);
    if (withCoords.length === 0) return;
    if (withCoords.length === 1) {
      map.setCenter(withCoords[0].coords!);
      map.setZoom(13);
      return;
    }
    const bounds = new window.google.maps.LatLngBounds();
    withCoords.forEach((p) => bounds.extend(p.coords!));
    map.fitBounds(bounds, BOUNDS_PADDING);
  }, [map, providers]);
  return null;
};

/**
 * Captures the Google Map instance into a parent ref so imperative
 * actions (panTo, setZoom) can be triggered from outside the Map context.
 */
const MapRefCapture: React.FC<{
  mapRef: React.MutableRefObject<google.maps.Map | null>;
}> = ({ mapRef }) => {
  const map = useMap();
  React.useEffect(() => {
    mapRef.current = map;
  }, [map, mapRef]);
  return null;
};

/**
 * Imperative marker layer — builds AdvancedMarker instances with React
 * content, groups them via MarkerClusterer, and rebuilds whenever the
 * visible provider set changes.
 *
 * Providers listed in `hiddenIds` are excluded from the map (their popup is
 * currently showing instead).
 */
const MarkerLayer: React.FC<{
  providers: ProviderData[];
  hiddenIds: Set<string>;
  onPinClick: (id: string) => void;
  onClusterClick: (providers: ProviderData[], position: google.maps.LatLngLiteral) => void;
}> = ({ providers, hiddenIds, onPinClick, onClusterClick }) => {
  const map = useMap();
  const markerLibrary = useMapsLibrary('marker');

  // Stash callbacks in a ref so the effect below doesn't re-run (and rebuild
  // every marker) when the parent passes fresh arrow-function references.
  const onPinClickRef = React.useRef(onPinClick);
  const onClusterClickRef = React.useRef(onClusterClick);
  React.useEffect(() => {
    onPinClickRef.current = onPinClick;
    onClusterClickRef.current = onClusterClick;
  }, [onPinClick, onClusterClick]);

  React.useEffect(() => {
    if (!map || !markerLibrary) return;

    const roots: Root[] = [];
    const markerToProvider = new Map<google.maps.marker.AdvancedMarkerElement, ProviderData>();

    const markers = providers
      .filter((p) => p.coords && !hiddenIds.has(p.id))
      .map((p) => {
        const el = document.createElement('div');
        const root = createRoot(el);
        // MapPin's own onClick stays for keyboard a11y (Enter/Space via its
        // onKeyDown). stopPropagation guards against the DOM click bubbling
        // to the Map's onClick and closing the popup the same frame it opens.
        root.render(
          <MapPin
            name={p.name}
            price={p.startingPrice}
            verified={p.verified}
            onClick={(e) => {
              e.stopPropagation();
              onPinClickRef.current(p.id);
            }}
          />,
        );
        roots.push(root);

        const marker = new markerLibrary.AdvancedMarkerElement({
          position: p.coords,
          content: el,
          gmpClickable: true,
        });
        // Also listen at the Google Maps level + stop the GMaps event so
        // Map's onClick can't fire when a pin is clicked via mouse. Safe to
        // fire twice with keyboard — handlePinClick is idempotent.
        marker.addListener('click', (event: google.maps.MapMouseEvent) => {
          event.stop();
          onPinClickRef.current(p.id);
        });
        markerToProvider.set(marker, p);
        return marker;
      });

    const clusterer = new MarkerClusterer({
      map,
      markers,
      algorithm: new GridAlgorithm({
        maxZoom: CLUSTER_MAX_ZOOM,
        gridSize: CLUSTER_GRID_SIZE,
      }),
      // Override the library's default "zoom to fit cluster" on click —
      // we open the cluster popup instead. The event shape the library
      // passes varies: sometimes a google.maps.MapMouseEvent (has .stop),
      // sometimes a plain DOM MouseEvent. Stop whichever we got so the
      // click doesn't also fire Map.onClick and clear our state.
      onClusterClick: (event, cluster) => {
        const anyEvent = event as unknown as {
          stop?: () => void;
          stopPropagation?: () => void;
          domEvent?: { stopPropagation?: () => void };
        };
        anyEvent.stop?.();
        anyEvent.stopPropagation?.();
        anyEvent.domEvent?.stopPropagation?.();

        const providersInCluster = cluster.markers
          .map((m) => markerToProvider.get(m as google.maps.marker.AdvancedMarkerElement))
          .filter((p): p is ProviderData => !!p);
        const clusterPosition =
          cluster.position instanceof window.google.maps.LatLng
            ? cluster.position.toJSON()
            : (cluster.position as google.maps.LatLngLiteral);
        onClusterClickRef.current(providersInCluster, clusterPosition);
      },
      renderer: {
        render: ({ count, position, markers: clusterMarkers }) => {
          const providersInCluster = clusterMarkers
            .map((m) => markerToProvider.get(m as google.maps.marker.AdvancedMarkerElement))
            .filter((p): p is ProviderData => !!p);
          const hasVerified = providersInCluster.some((p) => p.verified);

          const el = document.createElement('div');
          const root = createRoot(el);
          // Visual only — click is handled at the MarkerClusterer level above.
          root.render(<ClusterMarker count={count} hasVerified={hasVerified} />);
          roots.push(root);

          return new markerLibrary.AdvancedMarkerElement({
            position,
            content: el,
            gmpClickable: true,
          });
        },
      },
    });

    return () => {
      clusterer.clearMarkers();
      // Defer unmount so React doesn't warn about unmounting during render.
      setTimeout(() => {
        roots.forEach((r) => r.unmount());
      }, 0);
    };
  }, [map, markerLibrary, providers, hiddenIds]);

  return null;
};

/** Empty-state shown when no API key is configured or no providers have coords. */
const MapEmptyState: React.FC<{ reason: 'no-key' | 'no-coords' }> = ({ reason }) => (
  <Box sx={{ m: 'auto', textAlign: 'center', px: 3 }}>
    <Typography variant="body1" color="text.secondary" sx={{ mb: 0.5 }}>
      Map unavailable
    </Typography>
    <Typography variant="caption" color="text.secondary">
      {reason === 'no-key'
        ? 'Google Maps API key not configured.'
        : 'No provider locations to display.'}
    </Typography>
  </Box>
);

// ─── Component ──────────────────────────────────────────────────────────────

/**
 * Google Map showing provider pins with clustering + click-to-open popups.
 *
 * **Interaction model:**
 * - Clicking an individual pin **morphs** it into a `MapPopup` at the same
 *   coord. Clicking the map background reverts.
 * - Pins within `CLUSTER_GRID_SIZE` (70px) of each other collapse into a
 *   `ClusterMarker` — but only while zoomed out at level `CLUSTER_MAX_ZOOM`
 *   (13) or below. Zoom in past that and every pin shows individually.
 * - Clicking a cluster opens a `ClusterPopup` listing its providers
 *   (verified-first). Clicking a row **pans and zooms the map to that
 *   provider's location** (zoom 15 = past the clustering ceiling, so the
 *   other cluster members separate into their own pins around the selected
 *   one) and opens that provider's `MapPopup`. The cluster state is cleared
 *   — there's no back-to-list; the user's path forward is clear rather than
 *   hierarchical.
 *
 * **Viewport:** auto-fits to include every provider with coords on load and
 * when the list changes. Single-provider maps centre with zoom 13.
 *
 * **Empty states:** if no API key is set or no providers have coords, a
 * subtle empty state renders in place (no throw).
 *
 * Composes `MapPin` + `ClusterMarker` (atoms) + `MapPopup` + `ClusterPopup`
 * (molecules). Clustering via `@googlemaps/markerclusterer`.
 */
export const ProviderMap = React.forwardRef<ProviderMapHandle, ProviderMapProps>(
  (
    {
      providers,
      selectedProviderId,
      onSelectProvider,
      defaultCenter = FALLBACK_CENTER,
      defaultZoom = FALLBACK_ZOOM,
      apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY,
      externalisePopups = false,
      onActiveChange,
      sx,
    },
    ref,
  ) => {
    const [activeProviderId, setActiveProviderId] = React.useState<string | null>(null);
    const [activeCluster, setActiveCluster] = React.useState<ActiveCluster | null>(null);
    const [exiting, setExiting] = React.useState(false);
    const mapRef = React.useRef<google.maps.Map | null>(null);
    const exitTimerRef = React.useRef<number | null>(null);

    // Helper: cancel any pending exit timer so rapid clicks don't clobber
    // newly-opened popups with a leftover clear from a previous close.
    const cancelExit = React.useCallback(() => {
      if (exitTimerRef.current) {
        window.clearTimeout(exitTimerRef.current);
        exitTimerRef.current = null;
      }
      setExiting(false);
    }, []);

    React.useEffect(
      () => () => {
        if (exitTimerRef.current) window.clearTimeout(exitTimerRef.current);
      },
      [],
    );

    const withCoords = React.useMemo(() => providers.filter((p) => p.coords), [providers]);

    // External selection (e.g. list hover) force-opens a popup. Internal click wins.
    const effectiveProviderId = activeProviderId ?? selectedProviderId ?? null;

    const activeProvider = React.useMemo(
      () =>
        effectiveProviderId ? (withCoords.find((p) => p.id === effectiveProviderId) ?? null) : null,
      [withCoords, effectiveProviderId],
    );

    // Pins hidden from the map (because their popup is showing instead).
    const hiddenIds = React.useMemo(() => {
      const s = new Set<string>();
      if (effectiveProviderId) s.add(effectiveProviderId);
      if (activeCluster) {
        activeCluster.providers.forEach((p) => s.add(p.id));
      }
      return s;
    }, [effectiveProviderId, activeCluster]);

    const handlePinClick = React.useCallback(
      (id: string) => {
        cancelExit();
        setActiveProviderId(id);
        setActiveCluster(null);
      },
      [cancelExit],
    );

    const handleClusterClick = React.useCallback(
      (clusterProviders: ProviderData[], position: google.maps.LatLngLiteral) => {
        cancelExit();
        setActiveProviderId(null);
        setActiveCluster({ providers: clusterProviders, position });
      },
      [cancelExit],
    );

    /** Shared close path — animate the popup out (exiting=true triggers the
     *  CSS transition in MapPopup / ClusterPopup), then actually clear state
     *  after the transition completes so the pin can fade back in. */
    const closeWithExit = React.useCallback(() => {
      if (!activeProviderId && !activeCluster) return;
      if (exitTimerRef.current) window.clearTimeout(exitTimerRef.current);
      setExiting(true);
      exitTimerRef.current = window.setTimeout(() => {
        setActiveProviderId(null);
        setActiveCluster(null);
        setExiting(false);
        exitTimerRef.current = null;
      }, POPUP_EXIT_MS);
    }, [activeProviderId, activeCluster]);

    const handleMapClick = closeWithExit;
    const handleCloseCluster = closeWithExit;

    // Emit active-state changes when the caller is rendering popups externally.
    const onActiveChangeRef = React.useRef(onActiveChange);
    React.useEffect(() => {
      onActiveChangeRef.current = onActiveChange;
    }, [onActiveChange]);
    React.useEffect(() => {
      onActiveChangeRef.current?.({
        provider: activeProvider,
        cluster: activeCluster
          ? {
              providers: activeCluster.providers,
              position: {
                lat: activeCluster.position.lat,
                lng: activeCluster.position.lng,
              },
            }
          : null,
        exiting,
      });
    }, [activeProvider, activeCluster, exiting]);

    /** Cluster list → single-provider drill-in.
     *  Pans + zooms the map to the provider's coords (zoom 15 = past
     *  CLUSTER_MAX_ZOOM so nearby cluster members separate into individual
     *  pins around the selected one), then clears the cluster state and
     *  opens the single-provider popup. */
    const handleDrillIntoProvider = React.useCallback(
      (id: string) => {
        cancelExit();
        const provider = withCoords.find((p) => p.id === id);
        if (provider?.coords && mapRef.current) {
          mapRef.current.panTo(provider.coords);
          mapRef.current.setZoom(DRILL_IN_ZOOM);
        }
        setActiveProviderId(id);
        setActiveCluster(null);
      },
      [withCoords, cancelExit],
    );

    // Imperative handle for external callers (drawer close, cluster-row tap).
    React.useImperativeHandle(
      ref,
      () => ({
        clearActive: closeWithExit,
        drillIntoProvider: handleDrillIntoProvider,
      }),
      [closeWithExit, handleDrillIntoProvider],
    );

    const rootSx = [
      {
        position: 'relative' as const,
        display: 'flex',
        flex: 1,
        minHeight: 300,
        width: '100%',
        overflow: 'hidden',
        bgcolor: 'var(--fa-color-surface-cool)',
      },
      ...(Array.isArray(sx) ? sx : [sx]),
    ];

    // Empty states
    if (!apiKey) {
      return (
        <Box role="application" aria-label="Provider map" sx={rootSx}>
          <MapEmptyState reason="no-key" />
        </Box>
      );
    }
    if (withCoords.length === 0) {
      return (
        <Box role="application" aria-label="Provider map" sx={rootSx}>
          <MapEmptyState reason="no-coords" />
        </Box>
      );
    }

    return (
      <Box ref={ref} role="application" aria-label="Provider map" sx={rootSx}>
        <APIProvider apiKey={apiKey}>
          <GoogleMap
            defaultCenter={defaultCenter}
            defaultZoom={defaultZoom}
            mapId={MAP_ID}
            disableDefaultUI
            zoomControl
            gestureHandling="greedy"
            onClick={handleMapClick}
            style={{ width: '100%', height: '100%' }}
          >
            <FitBounds providers={withCoords} />
            <MapRefCapture mapRef={mapRef} />

            <MarkerLayer
              providers={withCoords}
              hiddenIds={hiddenIds}
              onPinClick={handlePinClick}
              onClusterClick={handleClusterClick}
            />

            {/* Internal popups — skipped when caller externalises them (e.g.
                mobile drawer). Active state still flows via onActiveChange. */}
            {!externalisePopups && activeProvider && (
              <AdvancedMarker position={activeProvider.coords!} zIndex={1000}>
                <MapPopup
                  name={activeProvider.name}
                  imageUrl={activeProvider.imageUrl}
                  price={activeProvider.startingPrice}
                  location={activeProvider.location}
                  rating={activeProvider.rating}
                  verified={activeProvider.verified}
                  exiting={exiting}
                  onClick={() => onSelectProvider(activeProvider.id)}
                />
              </AdvancedMarker>
            )}

            {/* Cluster list popup — shown while a cluster is active and no
                provider has been drilled into. Drilling clears activeCluster,
                which swaps this for the single-provider popup above. */}
            {!externalisePopups && activeCluster && !activeProviderId && (
              <AdvancedMarker position={activeCluster.position} zIndex={1000}>
                <ClusterPopup
                  providers={activeCluster.providers.map((p) => ({
                    id: p.id,
                    name: p.name,
                    location: p.location,
                    verified: p.verified,
                    rating: p.rating,
                    startingPrice: p.startingPrice,
                  }))}
                  exiting={exiting}
                  onSelectProvider={handleDrillIntoProvider}
                  onClose={handleCloseCluster}
                />
              </AdvancedMarker>
            )}
          </GoogleMap>
        </APIProvider>
      </Box>
    );
  },
);

ProviderMap.displayName = 'ProviderMap';
export default ProviderMap;
