import { useCallback, useRef, useEffect, useState, useMemo, memo } from 'react';
import { Map as MapGL, useControl } from 'react-map-gl/maplibre';
import type { MapRef } from 'react-map-gl/maplibre';
import { MapboxOverlay } from '@deck.gl/mapbox';
import { H3HexagonLayer } from '@deck.gl/geo-layers';
import { IconLayer, TextLayer } from '@deck.gl/layers';
import type { PickingInfo } from '@deck.gl/core';
import 'maplibre-gl/dist/maplibre-gl.css';
import type { HexagonData, ViewState, ViewChangeParams, Bounds, POI, FeatureMeta } from '../types';
import {
  GRADIENT,
  normalizedToColor,
  countToColor,
  zoomToResolution,
  getBoundsFromViewState,
  emojiToTwemojiUrl,
  MAP_STYLE_LIGHT,
  MAP_STYLE_DARK,
} from '../lib/map-utils';
import PostcodeSearch from './PostcodeSearch';
import MapLegend from './MapLegend';

interface MapProps {
  data: HexagonData[];
  pois: POI[];
  onViewChange: (params: ViewChangeParams) => void;
  viewFeature: string | null;
  colorRange: [number, number] | null;
  filterRange: [number, number] | null;
  viewSource: 'drag' | 'eye' | null;
  onCancelPin: () => void;
  features: FeatureMeta[];
  selectedHexagonId: string | null;
  hoveredHexagonId: string | null;
  onHexagonClick: (h3: string) => void;
  onHexagonHover: (h3: string | null) => void;
  initialViewState?: ViewState;
  theme?: 'light' | 'dark';
  screenshotMode?: boolean;
}

const INITIAL_VIEW: ViewState = {
  longitude: -1.5,
  latitude: 53.5,
  zoom: 6,
  pitch: 0,
};

interface Dimensions {
  width: number;
  height: number;
}

function DeckOverlay({
  layers,
  getTooltip,
}: {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  layers: any[];
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  getTooltip: any;
}) {
  const overlay = useControl(() => new MapboxOverlay({ interleaved: true }));
  const prevLayersRef = useRef(layers);
  const prevTooltipRef = useRef(getTooltip);
  if (layers !== prevLayersRef.current || getTooltip !== prevTooltipRef.current) {
    prevLayersRef.current = layers;
    prevTooltipRef.current = getTooltip;
    overlay.setProps({ layers, getTooltip });
  }
  return null;
}

export default memo(function Map({
  data,
  pois,
  onViewChange,
  viewFeature,
  colorRange,
  filterRange,
  viewSource,
  onCancelPin,
  features,
  selectedHexagonId,
  hoveredHexagonId,
  onHexagonClick,
  onHexagonHover,
  initialViewState,
  theme = 'light',
  screenshotMode = false,
}: MapProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [viewState, setViewState] = useState<ViewState>(initialViewState || INITIAL_VIEW);
  const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 });

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    const observer = new ResizeObserver((entries) => {
      const { width, height } = entries[0].contentRect;
      if (width > 0 && height > 0) {
        setDimensions({ width, height });
      }
    });

    observer.observe(container);
    return () => observer.disconnect();
  }, []);

  useEffect(() => {
    if (dimensions.width === 0 || dimensions.height === 0) return;

    const raw = getBoundsFromViewState(viewState, dimensions.width, dimensions.height);
    const resolution = zoomToResolution(viewState.zoom);

    const QUANT = 0.01;
    const bounds: Bounds = {
      south: Math.floor(raw.south / QUANT) * QUANT,
      west: Math.floor(raw.west / QUANT) * QUANT,
      north: Math.ceil(raw.north / QUANT) * QUANT,
      east: Math.ceil(raw.east / QUANT) * QUANT,
    };

    onViewChange({
      resolution,
      bounds,
      zoom: viewState.zoom,
      latitude: viewState.latitude,
      longitude: viewState.longitude,
    });
  }, [viewState, dimensions, onViewChange]);

  const handleMove = useCallback((evt: { viewState: ViewState }) => {
    setViewState(evt.viewState);
  }, []);

  const handleFlyTo = useCallback((lat: number, lng: number, zoom: number) => {
    setViewState((prev) => ({ ...prev, latitude: lat, longitude: lng, zoom }));
  }, []);

  const themeRef = useRef(theme);
  themeRef.current = theme;

  const handleMapLoad = useCallback(
    (evt: { target: MapRef['getMap'] extends () => infer M ? M : never }) => {
      const map = evt.target;
      if (themeRef.current === 'light') {
        for (const layer of map.getStyle().layers || []) {
          if (layer.type !== 'symbol') continue;
          map.setPaintProperty(layer.id, 'text-halo-color', 'rgba(255,255,255,1)');
          map.setPaintProperty(layer.id, 'text-halo-width', 2);
          map.setPaintProperty(layer.id, 'text-color', '#222');
        }
        for (const layer of map.getStyle().layers || []) {
          if (layer.id === 'water' || layer.id.startsWith('water')) {
            map.setPaintProperty(layer.id, 'fill-color', '#6baed6');
          }
        }
      }
      try {
        map.setLayoutProperty('building', 'visibility', 'none');
        map.setLayoutProperty('building-top', 'visibility', 'none');
      } catch {
        // layers may not exist in dark style
      }
    },
    []
  );

  const mapStyle = theme === 'dark' ? MAP_STYLE_DARK : MAP_STYLE_LIGHT;

  const [popupInfo, setPopupInfo] = useState<{
    x: number;
    y: number;
    name: string;
    category: string;
  } | null>(null);

  const handlePoiHover = useCallback((info: PickingInfo<POI>) => {
    if (info.object && info.x !== undefined && info.y !== undefined) {
      setPopupInfo({
        x: info.x,
        y: info.y,
        name: info.object.name,
        category: info.object.category,
      });
    } else {
      setPopupInfo(null);
    }
  }, []);

  const countRange = useMemo(() => {
    if (data.length === 0) return { min: 0, max: 1 };
    let min = Infinity;
    let max = -Infinity;
    for (const d of data) {
      const c = d.count as number;
      if (c < min) min = c;
      if (c > max) max = c;
    }
    if (min === max) return { min, max: min + 1 };
    return { min, max };
  }, [data]);

  const colorFeatureMeta = useMemo(
    () => (viewFeature ? features.find((f) => f.name === viewFeature) || null : null),
    [viewFeature, features]
  );

  const viewFeatureRef = useRef(viewFeature);
  viewFeatureRef.current = viewFeature;
  const colorRangeRef = useRef(colorRange);
  colorRangeRef.current = colorRange;
  const filterRangeRef = useRef(filterRange);
  filterRangeRef.current = filterRange;
  const colorFeatureMetaRef = useRef(colorFeatureMeta);
  colorFeatureMetaRef.current = colorFeatureMeta;
  const countRangeRef = useRef(countRange);
  countRangeRef.current = countRange;
  const selectedHexagonIdRef = useRef(selectedHexagonId);
  selectedHexagonIdRef.current = selectedHexagonId;
  const hoveredHexagonIdRef = useRef(hoveredHexagonId);
  hoveredHexagonIdRef.current = hoveredHexagonId;

  const onHexagonClickRef = useRef(onHexagonClick);
  onHexagonClickRef.current = onHexagonClick;
  const handleHexagonClick = useCallback((info: PickingInfo<HexagonData>) => {
    if (info.object && 'h3' in info.object) {
      onHexagonClickRef.current(info.object.h3);
    }
  }, []);

  const onHexagonHoverRef = useRef(onHexagonHover);
  onHexagonHoverRef.current = onHexagonHover;
  const handleHexagonHover = useCallback((info: PickingInfo<HexagonData>) => {
    if (info.object && 'h3' in info.object) {
      onHexagonHoverRef.current(info.object.h3);
    } else {
      onHexagonHoverRef.current(null);
    }
  }, []);

  const handlePoiHoverRef = useRef(handlePoiHover);
  handlePoiHoverRef.current = handlePoiHover;
  const stablePoiHover = useCallback((info: PickingInfo<POI>) => {
    handlePoiHoverRef.current(info);
  }, []);

  const colorTrigger = `${viewFeature}|${colorRange?.[0]}|${colorRange?.[1]}|${filterRange?.[0]}|${filterRange?.[1]}|${countRange.min}|${countRange.max}|${selectedHexagonId}|${hoveredHexagonId}`;

  const hexLayer = useMemo(
    () =>
      new H3HexagonLayer<HexagonData>({
        id: 'h3-hexagons',
        data,
        getHexagon: (d) => d.h3,
        getFillColor: (d) => {
          const vf = viewFeatureRef.current;
          const clr = colorRangeRef.current;
          const fr = filterRangeRef.current;
          const cfm = colorFeatureMetaRef.current;
          if (vf && clr && cfm) {
            const val = d[`min_${vf}`];
            if (val == null) return [128, 128, 128, 80] as [number, number, number, number];
            if (fr) {
              const minVal = d[`min_${vf}`] as number;
              const maxVal = d[`max_${vf}`] as number;
              if (maxVal < fr[0] || minVal > fr[1]) {
                return [180, 180, 180, 60] as [number, number, number, number];
              }
            }
            const range = clr[1] - clr[0];
            if (range === 0) return [...GRADIENT[0].color, 200] as [number, number, number, number];
            const t = ((val as number) - clr[0]) / range;
            const rgb = normalizedToColor(Math.max(0, Math.min(1, t)));
            return [...rgb, 200] as [number, number, number, number];
          }
          const cr = countRangeRef.current;
          const c = d.count as number;
          const t = (c - cr.min) / (cr.max - cr.min);
          return [...countToColor(Math.max(0, Math.min(1, t))), 200] as [
            number,
            number,
            number,
            number,
          ];
        },
        getLineColor: (d) => {
          if (d.h3 === selectedHexagonIdRef.current)
            return [255, 255, 255, 255] as [number, number, number, number];
          if (d.h3 === hoveredHexagonIdRef.current)
            return [29, 228, 195, 200] as [number, number, number, number];
          return [0, 0, 0, 0] as [number, number, number, number];
        },
        getLineWidth: (d) => {
          if (d.h3 === selectedHexagonIdRef.current) return 3;
          if (d.h3 === hoveredHexagonIdRef.current) return 2;
          return 0;
        },
        lineWidthUnits: 'pixels',
        updateTriggers: {
          getFillColor: [colorTrigger],
          getLineColor: [colorTrigger],
          getLineWidth: [colorTrigger],
        },
        extruded: false,
        pickable: true,
        opacity: 1,
        highPrecision: true,
        onClick: handleHexagonClick,
        onHover: handleHexagonHover,
        // @ts-expect-error beforeId is a MapboxOverlay interleave prop, not typed in LayerProps
        beforeId: 'waterway_label',
      }),
    [data, colorTrigger, handleHexagonClick, handleHexagonHover]
  );

  const poiLayer = useMemo(
    () =>
      new IconLayer<POI>({
        id: 'poi-icons',
        data: pois,
        getPosition: (d) => [d.lng, d.lat],
        getIcon: (d) => ({
          url: emojiToTwemojiUrl(d.emoji),
          width: 72,
          height: 72,
        }),
        getSize: 24,
        sizeMinPixels: 20,
        sizeMaxPixels: 40,
        pickable: true,
        onHover: stablePoiHover,
      }),
    [pois, stablePoiHover]
  );

  const postcodeData = useMemo(
    () => data.filter((d) => d.postcode && d.lat != null && d.lon != null),
    [data]
  );

  const showPostcodes = viewState.zoom >= 13;
  const postcodeLayer = useMemo(
    () =>
      showPostcodes
        ? new TextLayer<HexagonData>({
            id: 'postcode-labels',
            data: postcodeData,
            getPosition: (d) => [d.lon as number, d.lat as number],
            getText: (d) => d.postcode as string,
            getSize: 11,
            getColor: theme === 'dark' ? [220, 220, 220, 220] : [30, 30, 30, 220],
            getTextAnchor: 'middle',
            getAlignmentBaseline: 'center',
            fontFamily: 'Inter, system-ui, sans-serif',
            fontWeight: 600,
            outlineWidth: 2,
            outlineColor: theme === 'dark' ? [30, 30, 30, 200] : [255, 255, 255, 200],
            billboard: false,
            sizeUnits: 'pixels',
            sizeMinPixels: 10,
            sizeMaxPixels: 14,
          })
        : null,
    [postcodeData, showPostcodes, theme]
  );

  const layers = useMemo(
    () => [hexLayer, poiLayer, ...(postcodeLayer ? [postcodeLayer] : [])],
    [hexLayer, poiLayer, postcodeLayer]
  );

  return (
    <div className="flex-1 h-full relative" ref={containerRef}>
      <MapGL
        {...viewState}
        onMove={handleMove}
        onLoad={handleMapLoad as never}
        mapStyle={mapStyle}
        style={{ width: '100%', height: '100%' }}
        attributionControl={false}
        dragRotate={false}
        touchZoomRotate={true}
        touchPitch={false}
        keyboard={true}
        pitchWithRotate={false}
        minZoom={5}
        maxBounds={[-12, 49, 4, 62]}
      >
        <DeckOverlay layers={layers} getTooltip={null} />
      </MapGL>
      {screenshotMode ? (
        <div className="absolute inset-0 flex items-center justify-center z-20 pointer-events-none">
          <h1
            className="text-5xl font-bold text-white drop-shadow-lg"
            style={{ textShadow: '0 2px 8px rgba(0,0,0,0.6)' }}
          >
            Your perfect postcodes
          </h1>
        </div>
      ) : (
        <>
          <PostcodeSearch onFlyTo={handleFlyTo} />
          {viewSource === 'eye' && viewFeature && (
            <div className="absolute top-4 left-1/2 -translate-x-1/2 z-20 flex items-center gap-3 bg-white dark:bg-warm-800 rounded-lg shadow-lg px-5 py-3">
              <span className="text-lg font-semibold text-navy-950 dark:text-warm-100">
                Previewing &ldquo;{viewFeature}&rdquo;
              </span>
              <button
                onClick={onCancelPin}
                className="px-4 py-1.5 rounded-md bg-warm-200 dark:bg-warm-700 text-warm-700 dark:text-warm-200 hover:bg-warm-300 dark:hover:bg-warm-600 font-medium text-sm"
              >
                Cancel
              </button>
            </div>
          )}
          {viewFeature && colorRange && colorFeatureMeta ? (
            <MapLegend
              featureLabel={colorFeatureMeta.name}
              range={colorRange}
              showCancel={viewSource === 'eye'}
              onCancel={onCancelPin}
              mode="feature"
              enumValues={colorFeatureMeta.type === 'enum' ? colorFeatureMeta.values : undefined}
            />
          ) : (
            <MapLegend
              featureLabel="Property density"
              range={[0, 0]}
              showCancel={false}
              onCancel={onCancelPin}
              mode="density"
            />
          )}
          {popupInfo && (
            <div
              className="absolute pointer-events-none bg-white dark:bg-navy-800 rounded shadow-lg p-2 text-sm dark:text-warm-200"
              style={{
                left: popupInfo.x,
                top: popupInfo.y - 40,
                transform: 'translateX(-50%)',
                zIndex: 9999,
              }}
            >
              <strong>{popupInfo.name}</strong>
              <div className="text-gray-500 dark:text-warm-400 text-xs">{popupInfo.category}</div>
            </div>
          )}
        </>
      )}
    </div>
  );
});
