'use client';

import { memo, useCallback, useEffect, useRef, useState } from 'react';

import { useLocale } from 'next-intl';

import {
  APIProvider,
  Map as GoogleMap,
  InfoWindow,
  Marker,
  useMap,
} from '@vis.gl/react-google-maps';
import { ExternalLink, Maximize2, Minimize2 } from 'lucide-react';
import Supercluster from 'supercluster';

import { parseMapCoordinates } from '@/utils/mapCoordinates';
import { toNonEmptyText } from '@/utils/stringNormalization';

import type { CreationListItem } from '@/types/Creation';

import mapConfig from '@/config/mapConfig';
import { Link } from '@/i18n/navigation';

export type MapItemData = {
  id: number;
  alkotasAzonosito: string;
  imageKey: string | null;
  title: string | null;
  itemExtraData: string | null;
  extraData: string | null;
};

export type MapPointGroup = {
  geoData: {
    lat: number;
    lng: number;
  };
  itemList: MapItemData[];
};

const DEFAULT_ZOOM = 6;
const SINGLE_POINT_ZOOM = 11;
const HOVER_OPEN_DELAY_MS = 70;
const HOVER_CLOSE_DELAY_MS = 180;
const pinIconUrl = '/assets/images/icons/map-pin.svg';

export const convertMapData = (
  data: CreationListItem[],
  itemExtraDataFieldList: Array<keyof CreationListItem>,
  extraData?: string | null
): MapPointGroup[] => {
  const grouped = new Map<string, MapPointGroup>();

  for (const creation of data) {
    const coordinates = parseMapCoordinates(creation.koordinatak);
    if (!coordinates) continue;

    const itemExtraParts = itemExtraDataFieldList
      .map((fieldName) => toNonEmptyText(creation[fieldName]))
      .filter((part): part is string => Boolean(part));

    const item: MapItemData = {
      id: creation.id,
      alkotasAzonosito: creation.alkotasAzonosito,
      imageKey: creation.fokep ?? null,
      title: creation.nev,
      itemExtraData: itemExtraParts.length > 0 ? itemExtraParts.join(', ') : null,
      extraData: toNonEmptyText(extraData),
    };

    const key = `${coordinates.lat}|${coordinates.lng}`;
    const existing = grouped.get(key);

    if (existing) {
      existing.itemList.push(item);
      continue;
    }

    grouped.set(key, {
      geoData: coordinates,
      itemList: [item],
    });
  }

  return Array.from(grouped.values());
};

interface CreationMapProps {
  className?: string;
  data: MapPointGroup[];
  fitBoundsKey?: string | number;
  enablePointCards?: boolean;
  preferredInitialView?: {
    center: {
      lat: number;
      lng: number;
    };
    zoom: number;
  };
  preferredInitialViewKey?: string | number;
  options?: {
    gestureHandling?: google.maps.MapOptions['gestureHandling'];
    mapTypeId?: google.maps.MapTypeId | string;
  };
}

type MapPointFeatureProperties = {
  pointIndex: number;
};

type ClusterOrPointFeature =
  | Supercluster.ClusterFeature<Supercluster.AnyProps>
  | Supercluster.PointFeature<MapPointFeatureProperties>;

const superclusterOptions: Supercluster.Options<MapPointFeatureProperties, Supercluster.AnyProps> =
  {
    extent: 256,
    radius: 96,
    maxZoom: 19,
  };

function ViewportController({
  data,
  fitBoundsKey,
  preferredInitialView,
  preferredInitialViewKey,
}: {
  data: MapPointGroup[];
  fitBoundsKey?: string | number;
  preferredInitialView?: {
    center: {
      lat: number;
      lng: number;
    };
    zoom: number;
  };
  preferredInitialViewKey?: string | number;
}) {
  const map = useMap();
  const lastAppliedFitKeyRef = useRef<string | null>(null);

  useEffect(() => {
    if (!map || data.length === 0 || typeof window === 'undefined') return;

    const fitDataSignature = data
      .map((point) => `${point.geoData.lat}:${point.geoData.lng}`)
      .join('|');
    const nextFitKey = `${fitBoundsKey ?? 'default'}::${fitDataSignature}`;

    // Avoid re-fitting on transient map changes (e.g. map type toggle, user zoom/pan).
    if (lastAppliedFitKeyRef.current === nextFitKey) return;
    lastAppliedFitKeyRef.current = nextFitKey;

    const shouldUsePreferredInitialView =
      preferredInitialView &&
      String(fitBoundsKey ?? 'default') === String(preferredInitialViewKey ?? 'default');

    if (shouldUsePreferredInitialView) {
      map.panTo(preferredInitialView.center);
      map.setZoom(preferredInitialView.zoom);
      return;
    }

    if (data.length === 1) {
      map.panTo(data[0].geoData);
      map.setZoom(Math.max(map.getZoom() ?? DEFAULT_ZOOM, SINGLE_POINT_ZOOM));
      return;
    }

    const latLngBounds = new window.google.maps.LatLngBounds();
    for (const point of data) {
      latLngBounds.extend(point.geoData);
    }

    map.fitBounds(latLngBounds, 72);
  }, [map, data, fitBoundsKey, preferredInitialView, preferredInitialViewKey]);

  return null;
}

function ClusteredMarkersLayer({
  data,
  isMobileViewport,
  onOpenHoverCard,
  onCloseHoverCardWithDelay,
  onToggleTouchCard,
}: {
  data: MapPointGroup[];
  isMobileViewport: boolean;
  onOpenHoverCard: (pointIndex: number) => void;
  onCloseHoverCardWithDelay: (pointIndex: number) => void;
  onToggleTouchCard: (pointIndex: number) => void;
}) {
  const map = useMap();
  const clusterIndexRef = useRef<Supercluster<
    MapPointFeatureProperties,
    Supercluster.AnyProps
  > | null>(null);
  const [clusterFeatures, setClusterFeatures] = useState<ClusterOrPointFeature[]>([]);

  const refreshClusters = useCallback(() => {
    if (!map) return;
    const clusterIndex = clusterIndexRef.current;
    if (!clusterIndex) return;

    const bounds = map.getBounds();
    if (!bounds) return;

    const north = bounds.getNorthEast().lat();
    const east = bounds.getNorthEast().lng();
    const south = bounds.getSouthWest().lat();
    const west = bounds.getSouthWest().lng();
    const zoom = Math.max(0, Math.floor(map.getZoom() ?? DEFAULT_ZOOM));

    const bboxList: Array<[number, number, number, number]> =
      west <= east
        ? [[west, south, east, north]]
        : [
            [west, south, 180, north],
            [-180, south, east, north],
          ];

    const deduped = new Map<string, ClusterOrPointFeature>();
    for (const bbox of bboxList) {
      const features = clusterIndex.getClusters(bbox, zoom);

      for (const feature of features) {
        const featureProperties = feature.properties as
          | Supercluster.ClusterProperties
          | MapPointFeatureProperties;
        const dedupeKey =
          'cluster' in featureProperties
            ? `c-${featureProperties.cluster_id}`
            : `p-${featureProperties.pointIndex}`;

        deduped.set(dedupeKey, feature as ClusterOrPointFeature);
      }
    }

    setClusterFeatures(Array.from(deduped.values()));
  }, [map]);

  useEffect(() => {
    const pointFeatures: Supercluster.PointFeature<MapPointFeatureProperties>[] = data.map(
      (point, pointIndex) => ({
        type: 'Feature',
        geometry: {
          type: 'Point',
          coordinates: [point.geoData.lng, point.geoData.lat],
        },
        properties: { pointIndex },
      })
    );

    clusterIndexRef.current = new Supercluster(superclusterOptions).load(pointFeatures);
    refreshClusters();
  }, [data, refreshClusters]);

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

    const update = () => {
      refreshClusters();
    };

    const idleListener = map.addListener('idle', update);
    update();

    return () => {
      idleListener.remove();
    };
  }, [map, refreshClusters]);

  const handleClusterClick = useCallback(
    (feature: Supercluster.ClusterFeature<Supercluster.AnyProps>) => {
      if (!map) return;
      const clusterIndex = clusterIndexRef.current;
      if (!clusterIndex) return;

      const { cluster_id: clusterId } = feature.properties;
      const [lng, lat] = feature.geometry.coordinates;
      const expansionZoom = clusterIndex.getClusterExpansionZoom(clusterId);

      map.panTo({ lat, lng });
      map.setZoom(Math.min(19, expansionZoom));
    },
    [map]
  );

  return (
    <>
      {clusterFeatures.map((feature) => {
        const [lng, lat] = feature.geometry.coordinates;
        const clusterProperties = feature.properties as Supercluster.ClusterProperties;
        const isCluster = Boolean(clusterProperties.cluster);

        if (isCluster) {
          const markerSize = Math.min(
            46,
            Math.max(34, 22 + Math.log2(clusterProperties.point_count) * 6)
          );

          return (
            <Marker
              key={`cluster-${clusterProperties.cluster_id}-${lat}-${lng}`}
              position={{ lat, lng }}
              zIndex={2500 + clusterProperties.point_count}
              icon={{
                path: google.maps.SymbolPath.CIRCLE,
                fillColor: '#dfcf6f',
                fillOpacity: 0.95,
                strokeColor: '#3b3f58',
                strokeWeight: 2,
                scale: markerSize / 2,
              }}
              label={{
                text: String(clusterProperties.point_count_abbreviated),
                color: '#3b3f58',
                fontWeight: '700',
                fontSize: '12px',
              }}
              onClick={() =>
                handleClusterClick(feature as Supercluster.ClusterFeature<Supercluster.AnyProps>)
              }
            />
          );
        }

        const pointIndex = (feature as Supercluster.PointFeature<MapPointFeatureProperties>)
          .properties.pointIndex;
        const point = data[pointIndex];
        if (!point) return null;

        return (
          <Marker
            key={`point-${pointIndex}`}
            position={point.geoData}
            zIndex={100}
            icon={{
              url: pinIconUrl,
              size: new google.maps.Size(42, 54),
              scaledSize: new google.maps.Size(38, 50),
              anchor: new google.maps.Point(19, 50),
              labelOrigin: new google.maps.Point(19, 16),
            }}
            onMouseOver={isMobileViewport ? undefined : () => onOpenHoverCard(pointIndex)}
            onMouseOut={isMobileViewport ? undefined : () => onCloseHoverCardWithDelay(pointIndex)}
            onClick={() => {
              if (isMobileViewport) {
                onToggleTouchCard(pointIndex);
                return;
              }

              onOpenHoverCard(pointIndex);
            }}
          />
        );
      })}
    </>
  );
}

function PointCreationCards({ point, apiBaseUrl }: { point: MapPointGroup; apiBaseUrl: string }) {
  return (
    <div className="flex min-w-0 flex-col gap-4">
      {point.itemList.map((item) => (
        <Link
          key={`${item.id}-${item.alkotasAzonosito}`}
          href={{
            pathname: '/alkotas/[id]',
            params: { id: item.alkotasAzonosito },
          }}
          target="_blank"
          rel="noopener noreferrer"
          className="block min-w-0 max-w-full overflow-hidden transition-colors"
        >
          {item.imageKey && apiBaseUrl ? (
            <div
              className="mb-2 aspect-[2/1] w-full border border-[#151720]/10 bg-cover bg-center"
              style={{
                backgroundImage: `url(${apiBaseUrl}/imageRepository/getImage?key=${item.imageKey})`,
              }}
            />
          ) : null}

          <div className="min-w-0">
            {item.title ? (
              <div className="flex min-w-0 items-start gap-1.5">
                <div className="line-clamp-2 flex-1 break-words text-[15px] font-semibold text-[#0f172a]">
                  {item.title}
                </div>
                <ExternalLink size={13} className="mt-0.5 shrink-0 text-[#151720]/55" aria-hidden />
              </div>
            ) : null}
            {item.itemExtraData ? (
              <div className="mt-1 line-clamp-2 break-words text-[12px] font-semibold text-[#151720]/70">
                {item.itemExtraData}
              </div>
            ) : null}
            {item.extraData ? (
              <div className="mt-1.5 line-clamp-2 break-words text-[12px] font-semibold text-[#151720]/70">
                {item.extraData}
              </div>
            ) : null}
          </div>
        </Link>
      ))}
    </div>
  );
}

function CreationMap({
  className,
  data,
  fitBoundsKey,
  enablePointCards = true,
  preferredInitialView,
  preferredInitialViewKey,
  options,
}: CreationMapProps) {
  const locale = useLocale();
  const mapsApiKey = process.env.NEXT_PUBLIC_GMAPS_JAVASCRIPT_API_KEY || '';
  const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || '';
  const mapShellRef = useRef<HTMLDivElement | null>(null);
  const [hoveredPointIndex, setHoveredPointIndex] = useState<number | null>(null);
  const [isMobileViewport, setIsMobileViewport] = useState(false);
  const [isNativeFullscreen, setIsNativeFullscreen] = useState(false);
  const [isFallbackFullscreen, setIsFallbackFullscreen] = useState(false);
  const isCardHoveredRef = useRef(false);
  const hoveredPointIndexRef = useRef<number | null>(null);
  const openTimerRef = useRef<number | null>(null);
  const closeTimerRef = useRef<number | null>(null);
  const isFullscreenActive = isNativeFullscreen || isFallbackFullscreen;
  const fullscreenLabel = locale === 'hu' ? 'Teljes képernyő' : 'Fullscreen';
  const exitFullscreenLabel =
    locale === 'hu' ? 'Kilépés a teljes képernyős módból' : 'Exit fullscreen';

  const clearHoverTimers = useCallback(() => {
    if (openTimerRef.current !== null) {
      window.clearTimeout(openTimerRef.current);
      openTimerRef.current = null;
    }
    if (closeTimerRef.current !== null) {
      window.clearTimeout(closeTimerRef.current);
      closeTimerRef.current = null;
    }
  }, []);

  const closeHoverCardImmediate = useCallback(() => {
    clearHoverTimers();
    setHoveredPointIndex(null);
  }, [clearHoverTimers]);

  useEffect(() => {
    hoveredPointIndexRef.current = hoveredPointIndex;
  }, [hoveredPointIndex]);

  const openHoverCard = useCallback(
    (pointIndex: number) => {
      clearHoverTimers();
      if (hoveredPointIndexRef.current === pointIndex) return;

      openTimerRef.current = window.setTimeout(() => {
        setHoveredPointIndex((currentHovered) =>
          currentHovered === pointIndex ? currentHovered : pointIndex
        );
      }, HOVER_OPEN_DELAY_MS);
    },
    [clearHoverTimers]
  );

  const closeHoverCardWithDelay = useCallback(
    (pointIndex: number) => {
      clearHoverTimers();
      closeTimerRef.current = window.setTimeout(() => {
        setHoveredPointIndex((currentHovered) =>
          currentHovered === pointIndex && !isCardHoveredRef.current ? null : currentHovered
        );
      }, HOVER_CLOSE_DELAY_MS);
    },
    [clearHoverTimers]
  );

  const toggleTouchCard = useCallback(
    (pointIndex: number) => {
      clearHoverTimers();
      setHoveredPointIndex((currentHovered) => (currentHovered === pointIndex ? null : pointIndex));
    },
    [clearHoverTimers]
  );

  useEffect(() => {
    setHoveredPointIndex(null);
  }, [fitBoundsKey, data]);

  useEffect(() => {
    if (!enablePointCards) {
      setHoveredPointIndex(null);
    }
  }, [enablePointCards]);

  useEffect(
    () => () => {
      clearHoverTimers();
    },
    [clearHoverTimers]
  );

  useEffect(() => {
    if (typeof window === 'undefined' || !window.matchMedia) return;

    const mobileMediaQuery = window.matchMedia('(max-width: 767px)');
    const update = () => {
      setIsMobileViewport(mobileMediaQuery.matches);
    };

    update();
    mobileMediaQuery.addEventListener('change', update);
    return () => {
      mobileMediaQuery.removeEventListener('change', update);
    };
  }, []);

  const getNativeFullscreenElement = useCallback((): Element | null => {
    if (typeof document === 'undefined') return null;

    const webkitDocument = document as Document & {
      webkitFullscreenElement?: Element | null;
    };
    return document.fullscreenElement ?? webkitDocument.webkitFullscreenElement ?? null;
  }, []);

  const tryRequestNativeFullscreen = useCallback(async (): Promise<boolean> => {
    if (typeof document === 'undefined') return false;

    const shellElement = mapShellRef.current;
    if (!shellElement) return false;

    const webkitElement = shellElement as HTMLDivElement & {
      webkitRequestFullscreen?: () => Promise<void> | void;
    };
    const requestFullscreenFn =
      shellElement.requestFullscreen?.bind(shellElement) ??
      webkitElement.webkitRequestFullscreen?.bind(shellElement);

    if (!requestFullscreenFn) return false;

    try {
      await requestFullscreenFn();
    } catch {
      return false;
    }

    await new Promise<void>((resolve) => {
      window.setTimeout(resolve, 80);
    });

    return getNativeFullscreenElement() === shellElement;
  }, [getNativeFullscreenElement]);

  useEffect(() => {
    if (typeof document === 'undefined') return;

    const handleFullscreenChange = () => {
      setIsNativeFullscreen(getNativeFullscreenElement() === mapShellRef.current);
    };

    document.addEventListener('fullscreenchange', handleFullscreenChange);
    document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
    handleFullscreenChange();

    return () => {
      document.removeEventListener('fullscreenchange', handleFullscreenChange);
      document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
    };
  }, [getNativeFullscreenElement]);

  useEffect(() => {
    if (!isFallbackFullscreen || typeof document === 'undefined') return;

    const previousOverflow = document.body.style.overflow;
    document.body.style.overflow = 'hidden';

    return () => {
      document.body.style.overflow = previousOverflow;
    };
  }, [isFallbackFullscreen]);

  useEffect(() => {
    if (!isFallbackFullscreen) return;

    const handleKeydown = (event: KeyboardEvent) => {
      if (event.key === 'Escape') {
        setIsFallbackFullscreen(false);
      }
    };

    window.addEventListener('keydown', handleKeydown);
    return () => {
      window.removeEventListener('keydown', handleKeydown);
    };
  }, [isFallbackFullscreen]);

  const toggleFullscreen = useCallback(async () => {
    if (typeof document === 'undefined') return;

    if (isFallbackFullscreen) {
      setIsFallbackFullscreen(false);
      return;
    }

    if (getNativeFullscreenElement() === mapShellRef.current) {
      const webkitDocument = document as Document & {
        webkitExitFullscreen?: () => Promise<void> | void;
      };

      if (document.exitFullscreen) {
        await document.exitFullscreen();
      } else if (webkitDocument.webkitExitFullscreen) {
        await webkitDocument.webkitExitFullscreen();
      }
      return;
    }

    const enteredNativeFullscreen = await tryRequestNativeFullscreen();
    if (enteredNativeFullscreen) {
      setIsFallbackFullscreen(false);
      return;
    }

    setIsFallbackFullscreen(true);
  }, [getNativeFullscreenElement, isFallbackFullscreen, tryRequestNativeFullscreen]);

  const hoveredPoint = typeof hoveredPointIndex === 'number' ? data[hoveredPointIndex] : null;
  const mapUnavailableMessage =
    locale === 'hu' ? 'A terkep atmenetileg nem erheto el.' : 'Map is temporarily unavailable.';

  if (!mapsApiKey) {
    return (
      <div
        className={
          'flex h-full w-full items-center justify-center text-sm text-[#9f1239] ' +
          (className || '')
        }
      >
        {mapUnavailableMessage}
      </div>
    );
  }

  return (
    <div
      ref={mapShellRef}
      className={
        'relative isolate ' +
        (isFallbackFullscreen
          ? 'fixed inset-0 z-[2200] h-[100dvh] w-screen bg-[#0f172a]'
          : 'h-full w-full ' + (className || ''))
      }
    >
      <button
        type="button"
        onClick={() => {
          void toggleFullscreen();
        }}
        aria-label={isFullscreenActive ? exitFullscreenLabel : fullscreenLabel}
        title={isFullscreenActive ? exitFullscreenLabel : fullscreenLabel}
        className={
          'absolute right-3 top-3 inline-flex h-[46px] w-[46px] items-center justify-center border border-[#151720]/25 bg-white/95 text-[#0f172a] shadow-sm transition-colors hover:bg-white ' +
          (isFullscreenActive ? 'z-[1300]' : 'z-20')
        }
      >
        {isFullscreenActive ? <Minimize2 size={18} /> : <Maximize2 size={18} />}
      </button>
      <APIProvider apiKey={mapsApiKey} language={locale} region={locale === 'hu' ? 'HU' : 'EN'}>
        <GoogleMap
          defaultCenter={mapConfig.defaultCoordinates}
          defaultZoom={DEFAULT_ZOOM}
          minZoom={2}
          maxZoom={19}
          mapTypeId={options?.mapTypeId ?? 'hybrid'}
          gestureHandling={
            options?.gestureHandling ?? (isMobileViewport ? 'greedy' : 'cooperative')
          }
          disableDefaultUI
          zoomControl
          streetViewControl
          clickableIcons={false}
          onClick={enablePointCards ? closeHoverCardImmediate : undefined}
          className="h-full w-full"
        >
          <ViewportController
            data={data}
            fitBoundsKey={fitBoundsKey}
            preferredInitialView={preferredInitialView}
            preferredInitialViewKey={preferredInitialViewKey}
          />
          <ClusteredMarkersLayer
            data={data}
            isMobileViewport={isMobileViewport}
            onOpenHoverCard={enablePointCards ? openHoverCard : () => undefined}
            onCloseHoverCardWithDelay={enablePointCards ? closeHoverCardWithDelay : () => undefined}
            onToggleTouchCard={enablePointCards ? toggleTouchCard : () => undefined}
          />

          {enablePointCards && hoveredPoint ? (
            <InfoWindow
              position={hoveredPoint.geoData}
              headerDisabled
              maxWidth={isMobileViewport ? 300 : 340}
              pixelOffset={isMobileViewport ? [0, -34] : [0, -40]}
              onClose={closeHoverCardImmediate}
            >
              <div
                className={
                  isMobileViewport
                    ? 'w-[248px] max-w-[72vw] max-h-[220px] overflow-y-auto overflow-x-hidden bg-white'
                    : 'w-[292px] max-w-[76vw] max-h-[300px] overflow-y-auto overflow-x-hidden bg-white'
                }
                onMouseEnter={() => {
                  isCardHoveredRef.current = true;
                  clearHoverTimers();
                }}
                onMouseLeave={() => {
                  isCardHoveredRef.current = false;
                  if (typeof hoveredPointIndex === 'number') {
                    closeHoverCardWithDelay(hoveredPointIndex);
                  }
                }}
              >
                <PointCreationCards point={hoveredPoint} apiBaseUrl={apiBaseUrl} />
              </div>
            </InfoWindow>
          ) : null}
        </GoogleMap>
      </APIProvider>
    </div>
  );
}

const creationMapPropsEqual = (previous: CreationMapProps, next: CreationMapProps) =>
  previous.className === next.className &&
  previous.fitBoundsKey === next.fitBoundsKey &&
  previous.enablePointCards === next.enablePointCards &&
  previous.preferredInitialView?.center.lat === next.preferredInitialView?.center.lat &&
  previous.preferredInitialView?.center.lng === next.preferredInitialView?.center.lng &&
  previous.preferredInitialView?.zoom === next.preferredInitialView?.zoom &&
  previous.preferredInitialViewKey === next.preferredInitialViewKey &&
  previous.data === next.data &&
  previous.options?.gestureHandling === next.options?.gestureHandling &&
  previous.options?.mapTypeId === next.options?.mapTypeId;

const MemoizedCreationMap = memo(CreationMap, creationMapPropsEqual);

if (process.env.NODE_ENV !== 'production') {
  MemoizedCreationMap.displayName = 'CreationMap';
}

export default MemoizedCreationMap;
