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 { GeoJsonLayer, IconLayer, TextLayer } from '@deck.gl/layers';
import type { PickingInfo } from '@deck.gl/core';
import 'maplibre-gl/dist/maplibre-gl.css';
import type {
  HexagonData,
  PostcodeFeature,
  PostcodeProperties,
  ViewState,
  ViewChangeParams,
  POI,
  FeatureMeta,
} from '../../types';
import {
  GRADIENT,
  normalizedToColor,
  countToColor,
  zoomToResolution,
  getBoundsFromViewState,
  emojiToTwemojiUrl,
  getMapStyle,
  DENSITY_GRADIENT,
  DENSITY_GRADIENT_DARK,
} from '../../lib/map-utils';
import { INITIAL_VIEW_STATE, MAP_MIN_ZOOM, MAP_BOUNDS } from '../../lib/consts';
import PostcodeSearch, { type SearchedPostcode } from './PostcodeSearch';
import MapLegend from './MapLegend';
import HoverCard from './HoverCard';
import type { FeatureFilters } from '../../types';

/** Convert POI id (e.g. "n12345") to OpenStreetMap URL */
function osmIdToUrl(id: string): string | null {
  const match = id.match(/^([nwr])(\d+)$/);
  if (!match) return null;
  const typeMap: Record<string, string> = { n: 'node', w: 'way', r: 'relation' };
  return `https://www.openstreetmap.org/${typeMap[match[1]]}/${match[2]}`;
}

interface MapProps {
  data: HexagonData[];
  postcodeData: PostcodeFeature[];
  usePostcodeView: boolean;
  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: (id: string, isPostcode?: boolean) => void;
  onHexagonHover: (h3: string | null, x?: number, y?: number) => void;
  initialViewState?: ViewState;
  theme?: 'light' | 'dark';
  screenshotMode?: boolean;
  ogMode?: boolean;
  filters?: FeatureFilters;
  searchedPostcode?: SearchedPostcode | null;
  onPostcodeSearched?: (postcode: SearchedPostcode | null) => void;
}


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,
  postcodeData,
  usePostcodeView,
  pois,
  onViewChange,
  viewFeature,
  colorRange,
  filterRange,
  viewSource,
  onCancelPin,
  features,
  selectedHexagonId,
  hoveredHexagonId,
  onHexagonClick,
  onHexagonHover,
  initialViewState,
  theme = 'light',
  screenshotMode = false,
  ogMode = false,
  filters = {},
  searchedPostcode,
  onPostcodeSearched,
}: MapProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [viewState, setViewState] = useState<ViewState>(initialViewState || INITIAL_VIEW_STATE);
  const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 });
  const [hoverPosition, setHoverPosition] = useState<{ x: number; y: number } | null>(null);

  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;

    // Send exact viewport bounds - server will filter to only return
    // hexagons/postcodes that intersect this precise AABB
    const bounds = getBoundsFromViewState(viewState, dimensions.width, dimensions.height);
    const resolution = zoomToResolution(viewState.zoom);

    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 }) => {
      // Road opacity is set in getMapStyle
    },
    []
  );

  const mapStyle = useMemo(() => getMapStyle(theme), [theme]);

  const [popupInfo, setPopupInfo] = useState<{
    x: number;
    y: number;
    name: string;
    category: string;
    id: 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,
        id: info.object.id,
      });
    } 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 && info.x !== undefined && info.y !== undefined) {
      setHoverPosition({ x: info.x, y: info.y });
      onHexagonHoverRef.current(info.object.h3, info.x, info.y);
    } else {
      setHoverPosition(null);
      onHexagonHoverRef.current(null);
    }
  }, []);

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

  // Compute count range for postcodes (similar to hexagons)
  const postcodeCountRange = useMemo(() => {
    if (postcodeData.length === 0) return { min: 0, max: 1 };
    let min = Infinity;
    let max = -Infinity;
    for (const d of postcodeData) {
      const c = d.properties.count;
      if (c < min) min = c;
      if (c > max) max = c;
    }
    if (min === max) return { min, max: min + 1 };
    return { min, max };
  }, [postcodeData]);

  const postcodeCountRangeRef = useRef(postcodeCountRange);
  postcodeCountRangeRef.current = postcodeCountRange;

  // Track selected/hovered postcode for styling
  const [selectedPostcode, setSelectedPostcode] = useState<string | null>(null);
  const [hoveredPostcode, setHoveredPostcode] = useState<string | null>(null);
  const selectedPostcodeRef = useRef(selectedPostcode);
  selectedPostcodeRef.current = selectedPostcode;
  const hoveredPostcodeRef = useRef(hoveredPostcode);
  hoveredPostcodeRef.current = hoveredPostcode;

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const handlePostcodeClick = useCallback((info: PickingInfo<any>) => {
    const pc = info.object?.properties?.postcode;
    if (pc) {
      setSelectedPostcode((prev) => (prev === pc ? null : pc));
      onHexagonClickRef.current(pc, true);
    }
  }, []);

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const handlePostcodeHoverCallback = useCallback((info: PickingInfo<any>) => {
    const pc = info.object?.properties?.postcode;
    if (pc && info.x !== undefined && info.y !== undefined) {
      setHoveredPostcode(pc);
      setHoverPosition({ x: info.x, y: info.y });
      onHexagonHoverRef.current(pc, info.x, info.y);
    } else {
      setHoveredPostcode(null);
      setHoverPosition(null);
      onHexagonHoverRef.current(null);
    }
  }, []);

  const isDark = theme === 'dark';
  const densityGradient = isDark ? DENSITY_GRADIENT_DARK : DENSITY_GRADIENT;
  const densityGradientRef = useRef(densityGradient);
  densityGradientRef.current = densityGradient;
  const isDarkRef = useRef(isDark);
  isDarkRef.current = isDark;

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

  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;
          const dark = isDarkRef.current;
          if (vf && clr && cfm) {
            const val = d[`min_${vf}`];
            if (val == null) return (dark ? [80, 70, 65, 80] : [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 (dark ? [60, 55, 50, 60] : [180, 180, 180, 60]) as [number, number, number, number];
              }
            }
            const range = clr[1] - clr[0];
            if (range === 0) return [...GRADIENT[0].color, 255] 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, 255] 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)), densityGradientRef.current), 255] 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: 'landuse_park',
      }),
    [data, colorTrigger, handleHexagonClick, handleHexagonHover]
  );

  const postcodeLayer = useMemo(
    () =>
      new GeoJsonLayer<PostcodeProperties>({
        id: 'postcode-polygons',
        data: postcodeData as PostcodeFeature[],
        getFillColor: (f) => {
          const d = f.properties;
          const vf = viewFeatureRef.current;
          const clr = colorRangeRef.current;
          const fr = filterRangeRef.current;
          const cfm = colorFeatureMetaRef.current;
          const dark = isDarkRef.current;
          if (vf && clr && cfm) {
            const val = d[`min_${vf}`];
            if (val == null) return (dark ? [80, 70, 65, 80] : [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 (dark ? [60, 55, 50, 60] : [180, 180, 180, 60]) as [number, number, number, number];
              }
            }
            const range = clr[1] - clr[0];
            if (range === 0) return [...GRADIENT[0].color, 255] 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, 255] as [number, number, number, number];
          }
          const cr = postcodeCountRangeRef.current;
          const c = d.count;
          const t = (c - cr.min) / (cr.max - cr.min);
          return [...countToColor(Math.max(0, Math.min(1, t)), densityGradientRef.current), 255] as [
            number,
            number,
            number,
            number,
          ];
        },
        getLineColor: (f) => {
          const pc = f.properties.postcode;
          const dark = isDarkRef.current;
          if (pc === selectedPostcodeRef.current)
            return [255, 255, 255, 255] as [number, number, number, number];
          if (pc === hoveredPostcodeRef.current)
            return [29, 228, 195, 200] as [number, number, number, number];
          return (dark ? [180, 170, 160, 100] : [100, 100, 100, 150]) as [number, number, number, number];
        },
        getLineWidth: (f) => {
          const pc = f.properties.postcode;
          if (pc === selectedPostcodeRef.current) return 3;
          if (pc === hoveredPostcodeRef.current) return 2;
          return 1;
        },
        lineWidthUnits: 'pixels',
        updateTriggers: {
          getFillColor: [postcodeColorTrigger],
          getLineColor: [postcodeColorTrigger],
          getLineWidth: [postcodeColorTrigger],
        },
        extruded: false,
        pickable: true,
        onClick: handlePostcodeClick,
        onHover: handlePostcodeHoverCallback,
      }),
    [postcodeData, postcodeColorTrigger, handlePostcodeClick, handlePostcodeHoverCallback]
  );

  const postcodeLabelsLayer = useMemo(
    () =>
      new TextLayer<PostcodeFeature>({
        id: 'postcode-labels',
        data: postcodeData,
        getPosition: (f) => f.properties.centroid,
        getText: (f) => f.properties.postcode,
        getSize: 12,
        getColor: theme === 'dark' ? [255, 255, 255, 240] : [40, 40, 40, 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],
        sizeUnits: 'pixels',
        sizeMinPixels: 10,
        sizeMaxPixels: 14,
        billboard: false,
        pickable: false,
      }),
    [postcodeData, theme]
  );

  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]
  );

  // Check if the searched postcode has data (passes current filters)
  const searchedPostcodeHasData = useMemo(() => {
    if (!searchedPostcode) return false;
    return postcodeData.some((f) => f.properties.postcode === searchedPostcode.postcode);
  }, [searchedPostcode, postcodeData]);

  // Highlight layer for searched postcode
  const searchedPostcodeHighlightLayer = useMemo(() => {
    if (!searchedPostcode) return null;
    const hasData = searchedPostcodeHasData;
    const feature = {
      type: 'Feature' as const,
      geometry: searchedPostcode.geometry,
      properties: {},
    };
    return new GeoJsonLayer({
      id: 'searched-postcode-highlight',
      data: [feature],
      getFillColor: hasData
        ? [29, 228, 195, 40] // teal tint when has data
        : [255, 180, 0, 30], // orange tint when filtered out
      getLineColor: hasData
        ? [29, 228, 195, 255] // solid teal when has data
        : [255, 180, 0, 200], // orange when filtered out (no matching properties)
      getLineWidth: hasData ? 4 : 3,
      lineWidthUnits: 'pixels',
      stroked: true,
      filled: true,
      pickable: false,
    });
  }, [searchedPostcode, searchedPostcodeHasData]);

  const layers = useMemo(() => {
    const baseLayers = usePostcodeView
      ? [postcodeLayer, postcodeLabelsLayer, poiLayer]
      : [hexLayer, poiLayer];
    if (searchedPostcodeHighlightLayer) {
      return [...baseLayers, searchedPostcodeHighlightLayer];
    }
    return baseLayers;
  }, [usePostcodeView, hexLayer, postcodeLayer, postcodeLabelsLayer, poiLayer, searchedPostcodeHighlightLayer]);

  const handleMouseLeave = useCallback(() => {
    setHoverPosition(null);
    setHoveredPostcode(null);
    setPopupInfo(null);
    onHexagonHoverRef.current(null);
  }, []);

  return (
    <div className="flex-1 h-full relative" ref={containerRef} onMouseLeave={handleMouseLeave}>
      <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={MAP_MIN_ZOOM}
        maxBounds={MAP_BOUNDS}
      >
        <DeckOverlay layers={layers} getTooltip={null} />
      </MapGL>
      {screenshotMode ? (
        ogMode ? (
          <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>
        ) : null
      ) : (
        <>
          <PostcodeSearch onFlyTo={handleFlyTo} onPostcodeSearched={onPostcodeSearched} />
          {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-white">
                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-white 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}
              theme={theme}
            />
          ) : (
            <MapLegend
              featureLabel="Property density"
              range={[0, 0]}
              showCancel={false}
              onCancel={onCancelPin}
              mode="density"
              theme={theme}
            />
          )}
          {popupInfo && (
            <div
              className="absolute bg-white dark:bg-warm-800 rounded shadow-lg p-2 text-sm dark:text-white"
              style={{
                left: popupInfo.x,
                top: popupInfo.y - 40,
                transform: 'translateX(-50%)',
                zIndex: 9999,
              }}
            >
              <strong className="dark:text-white">{popupInfo.name}</strong>
              <div className="text-warm-500 dark:text-warm-300 text-xs">{popupInfo.category}</div>
              {osmIdToUrl(popupInfo.id) && (
                <a
                  href={osmIdToUrl(popupInfo.id)!}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 text-xs"
                >
                  View on OSM
                </a>
              )}
            </div>
          )}
          {hoverPosition && hoveredHexagonId && hoveredHexagonId !== selectedHexagonId && (
            <HoverCard
              x={hoverPosition.x}
              y={hoverPosition.y}
              id={hoveredHexagonId}
              isPostcode={usePostcodeView}
              data={
                usePostcodeView
                  ? postcodeData.find((f) => f.properties.postcode === hoveredHexagonId)
                      ?.properties || null
                  : data.find((d) => d.h3 === hoveredHexagonId) || null
              }
              filters={filters}
            />
          )}
        </>
      )}
    </div>
  );
});
