import { useCallback, useRef, useState, useMemo, useEffect } from 'react';
import { H3HexagonLayer } from '@deck.gl/geo-layers';
import { GeoJsonLayer, TextLayer, ScatterplotLayer } from '@deck.gl/layers';
import { cellToBoundary, isValidCell } from 'h3-js';
import type { PickingInfo } from '@deck.gl/core';
import type {
  HexagonData,
  PostcodeFeature,
  PostcodeProperties,
  PostcodeGeometry,
  POI,
  FeatureMeta,
  Bounds,
  ActualListing,
} from '../types';
import {
  DENSITY_GRADIENT,
  DENSITY_GRADIENT_DARK,
  getEnumPaletteForFeature,
  getFeatureGradient,
} from '../lib/consts';
import { getFeatureFillColor } from '../lib/map-utils';
import type { TravelTimeEntry } from './useTravelTime';
import { usePoiLayers } from './usePoiLayers';
import { useListingLayers } from './useListingLayers';
import { MarchingAntsExtension } from '../lib/MarchingAntsExtension';
import { PieHexExtension } from '../lib/PieHexExtension';
import { normalizeColorOpacity } from '../lib/color-opacity';

interface UseDeckLayersProps {
  data: HexagonData[];
  postcodeData: PostcodeFeature[];
  usePostcodeView: boolean;
  zoom: number;
  pois: POI[];
  actualListings: ActualListing[];
  viewFeature: string | null;
  colorRange: [number, number] | null;
  filterRange: [number, number] | null;
  features: FeatureMeta[];
  selectedHexagonId: string | null;
  hoveredHexagonId: string | null;
  onHexagonClick: (id: string, isPostcode?: boolean, geometry?: PostcodeGeometry) => void;
  onHexagonHover: (h3: string | null, x?: number, y?: number) => void;
  theme: 'light' | 'dark';
  selectedPostcodeGeometry?: PostcodeGeometry | null;
  currentLocation?: { lat: number; lng: number } | null;
  bounds?: Bounds | null;
  travelTimeEntries?: TravelTimeEntry[];
  mapDataBeforeId: string;
  colorOpacity?: number;
}

/** Normalize a distribution count array to [0..1] ratios, padded to 10 values. */
function distToRatios(dist: unknown): number[] {
  if (!Array.isArray(dist) || dist.length === 0) return [1, 0, 0, 0, 0, 0, 0, 0, 0, 0];
  let total = 0;
  for (let i = 0; i < dist.length; i++) total += (dist[i] as number) || 0;
  if (total === 0) return [1, 0, 0, 0, 0, 0, 0, 0, 0, 0];
  const r = new Array<number>(10).fill(0);
  for (let i = 0; i < Math.min(dist.length, 10); i++) r[i] = ((dist[i] as number) || 0) / total;
  return r;
}

function requireEnumPalette(
  palette: [number, number, number][] | null
): [number, number, number][] {
  if (!palette) {
    throw new Error('Enum layer requested without an enum color palette');
  }
  return palette;
}

type RgbaColor = [number, number, number, number];

function withColorOpacity(color: RgbaColor, opacity: number): RgbaColor {
  if (opacity >= 1) return color;
  return [color[0], color[1], color[2], Math.round(color[3] * opacity)];
}

export function useDeckLayers({
  data,
  postcodeData,
  usePostcodeView,
  zoom,
  pois,
  actualListings,
  viewFeature,
  colorRange,
  filterRange,
  features,
  selectedHexagonId,
  hoveredHexagonId,
  onHexagonClick,
  onHexagonHover,
  theme,
  selectedPostcodeGeometry,
  currentLocation,
  bounds: viewportBounds,
  travelTimeEntries = [],
  mapDataBeforeId,
  colorOpacity = 1,
}: UseDeckLayersProps) {
  const [hoverPosition, setHoverPosition] = useState<{ x: number; y: number } | null>(null);
  const [hoveredPostcode, setHoveredPostcode] = useState<string | null>(null);

  // Marching ants animation
  const [marchTime, setMarchTime] = useState(0);
  const hasSelection = selectedPostcodeGeometry != null || selectedHexagonId != null;
  useEffect(() => {
    if (!hasSelection) return;
    setMarchTime(0);
    const id = setInterval(() => setMarchTime((t) => (t + 0.3) % 10000), 50);
    return () => clearInterval(id);
  }, [hasSelection]);

  const isDark = theme === 'dark';
  const densityGradient = isDark ? DENSITY_GRADIENT_DARK : DENSITY_GRADIENT;
  const { poiLayers, visiblePois, popupInfo, clearPopupInfo } = usePoiLayers({
    pois,
    zoom,
    isDark,
  });
  const { listingLayers, listingPopup, clearListingPopup } = useListingLayers({
    listings: actualListings,
    zoom,
    isDark,
  });

  // --- Refs for deck.gl accessors ---
  const viewFeatureRef = useRef(viewFeature);
  viewFeatureRef.current = viewFeature;
  const colorRangeRef = useRef(colorRange);
  colorRangeRef.current = colorRange;
  const filterRangeRef = useRef(filterRange);
  filterRangeRef.current = filterRange;
  const isDarkRef = useRef(isDark);
  isDarkRef.current = isDark;
  const densityGradientRef = useRef(densityGradient);
  densityGradientRef.current = densityGradient;
  const normalizedColorOpacity = normalizeColorOpacity(colorOpacity);
  const colorOpacityRef = useRef(normalizedColorOpacity);
  colorOpacityRef.current = normalizedColorOpacity;
  const featureGradientRef = useRef(getFeatureGradient(viewFeature));
  featureGradientRef.current = getFeatureGradient(viewFeature);
  const selectedHexagonIdRef = useRef(selectedHexagonId);
  selectedHexagonIdRef.current = selectedHexagonId;
  const selectedHexagonSelectionRef = useRef<string | null>(null);
  selectedHexagonSelectionRef.current = selectedPostcodeGeometry ? null : selectedHexagonId;
  const selectedPostcodeIdRef = useRef<string | null>(null);
  selectedPostcodeIdRef.current = selectedPostcodeGeometry ? selectedHexagonId : null;
  const hoveredHexagonIdRef = useRef(hoveredHexagonId);
  hoveredHexagonIdRef.current = hoveredHexagonId;
  const hoveredPostcodeRef = useRef(hoveredPostcode);
  hoveredPostcodeRef.current = hoveredPostcode;

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

  // Track enum value count for discrete coloring (0 = numeric/continuous)
  const enumCountRef = useRef(0);
  enumCountRef.current =
    colorFeatureMeta?.type === 'enum' && colorFeatureMeta.values
      ? colorFeatureMeta.values.length
      : 0;

  const enumPalette =
    viewFeature && colorFeatureMeta?.type === 'enum' && colorFeatureMeta.values
      ? getEnumPaletteForFeature(viewFeature, colorFeatureMeta.values)
      : null;
  const enumPaletteRef = useRef(enumPalette);
  enumPaletteRef.current = enumPalette;

  const countRange = useMemo(() => {
    if (data.length === 0) return { min: 0, max: 1 };
    let min = Infinity;
    let max = -Infinity;
    for (const d of data) {
      if (viewportBounds) {
        if (
          d.lat < viewportBounds.south ||
          d.lat > viewportBounds.north ||
          d.lon < viewportBounds.west ||
          d.lon > viewportBounds.east
        )
          continue;
      }
      const c = d.count as number;
      if (c <= 0) continue;
      if (c < min) min = c;
      if (c > max) max = c;
    }
    if (min === Infinity) return { min: 0, max: 1 };
    if (min === max) return { min, max: min + 1 };
    return { min, max };
  }, [data, viewportBounds]);

  const countRangeRef = useRef(countRange);
  countRangeRef.current = countRange;

  const postcodeCountRange = useMemo(() => {
    if (postcodeData.length === 0) return { min: 0, max: 1 };
    let min = Infinity;
    let max = -Infinity;
    for (const d of postcodeData) {
      if (viewportBounds) {
        const [lng, lat] = d.properties.centroid as [number, number];
        if (
          lat < viewportBounds.south ||
          lat > viewportBounds.north ||
          lng < viewportBounds.west ||
          lng > viewportBounds.east
        )
          continue;
      }
      const c = d.properties.count;
      if (c <= 0) continue;
      if (c < min) min = c;
      if (c > max) max = c;
    }
    if (min === Infinity) return { min: 0, max: 1 };
    if (min === max) return { min, max: min + 1 };
    return { min, max };
  }, [postcodeData, viewportBounds]);

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

  // --- Click/hover handlers ---
  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);
    }
  }, []);

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

  // 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);
    }
  }, []);

  // --- Color triggers ---
  const ttTrigger = useMemo(() => {
    const parts: string[] = [];
    for (let i = 0; i < travelTimeEntries.length; i++) {
      const entry = travelTimeEntries[i];
      parts.push(`${i}:${entry.slug}|${entry.timeRange?.[0]}|${entry.timeRange?.[1]}`);
    }
    return parts.join(';');
  }, [travelTimeEntries]);

  const selectedHexagonKey = selectedPostcodeGeometry ? '' : (selectedHexagonId ?? '');
  const selectedPostcodeKey = selectedPostcodeGeometry ? (selectedHexagonId ?? '') : '';
  const colorTrigger = `${viewFeature}|${colorRange?.[0]}|${colorRange?.[1]}|${filterRange?.[0]}|${filterRange?.[1]}|${countRange.min}|${countRange.max}|${selectedHexagonKey}|${hoveredHexagonId}|${theme}|${normalizedColorOpacity}|${ttTrigger}`;
  const postcodeColorTrigger = `${viewFeature}|${colorRange?.[0]}|${colorRange?.[1]}|${filterRange?.[0]}|${filterRange?.[1]}|${postcodeCountRange.min}|${postcodeCountRange.max}|${selectedPostcodeKey}|${hoveredPostcode}|${theme}|${normalizedColorOpacity}|${ttTrigger}`;

  // --- Layers ---
  // PieHexExtension uses the canonical deck.gl v9 pattern: defaultProps with
  // type:'accessor' + stepMode:'dynamic'. LayerExtension.getSubLayerProps()
  // wraps accessors via getSubLayerAccessor() which unwraps __source.object,
  // letting accessor functions work through CompositeLayer sublayer chains.
  const hexLayer = useMemo(() => {
    const isEnum = enumCountRef.current > 0;
    const distKey = viewFeatureRef.current ? `dist_${viewFeatureRef.current}` : '';

    // Per-render memo: each of getRatios0/1/2 would otherwise call distToRatios
    // on the same row, tripling the work. Cache by row reference.
    const ratiosCache = new WeakMap<HexagonData, number[]>();
    const getRatios = (d: HexagonData): number[] => {
      let r = ratiosCache.get(d);
      if (!r) {
        r = distToRatios(d[distKey]);
        ratiosCache.set(d, r);
      }
      return r;
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pieProps: any = isEnum
      ? {
          extensions: [new PieHexExtension(requireEnumPalette(enumPaletteRef.current))],
          getCenter: (d: HexagonData) => [d.lon, d.lat],
          getRatios0: (d: HexagonData) => {
            const r = getRatios(d);
            return [r[0], r[1], r[2], r[3]];
          },
          getRatios1: (d: HexagonData) => {
            const r = getRatios(d);
            return [r[4], r[5], r[6], r[7]];
          },
          getRatios2: (d: HexagonData) => {
            const r = getRatios(d);
            return [r[8], r[9]];
          },
          updateTriggers: {
            getCenter: [colorTrigger, data],
            getRatios0: [colorTrigger, data],
            getRatios1: [colorTrigger, data],
            getRatios2: [colorTrigger, data],
          },
        }
      : {};

    return new H3HexagonLayer<HexagonData>({
      id: isEnum ? 'h3-hexagons-pie' : 'h3-hexagons',
      data,
      getHexagon: (d) => d.h3,
      getFillColor: (d) => {
        if ((d.count as number) <= 0) {
          return [0, 0, 0, 0] as [number, number, number, number];
        }
        const fill = (color: RgbaColor) => withColorOpacity(color, colorOpacityRef.current);
        const dark = isDarkRef.current;
        const vf = viewFeatureRef.current;
        const clr = colorRangeRef.current;
        const fr = filterRangeRef.current;
        const cfm = colorFeatureMetaRef.current;

        if (vf && clr) {
          if (vf.startsWith('tt_')) {
            const ttVal = d[`avg_${vf}`];
            if (ttVal == null) {
              return fill(dark ? [80, 70, 65, 80] : [128, 128, 128, 80]);
            }
            const ttMin = (d[`min_${vf}`] as number) ?? ttVal;
            const ttMax = (d[`max_${vf}`] as number) ?? ttVal;
            return fill(
              getFeatureFillColor(
                ttVal as number,
                ttMin as number,
                ttMax as number,
                clr,
                fr,
                0,
                densityGradientRef.current,
                dark,
                255,
                0,
                undefined,
                featureGradientRef.current
              )
            );
          }

          if (cfm) {
            const val = d[`avg_${vf}`] ?? d[`min_${vf}`];
            const minVal = d[`min_${vf}`] as number | undefined;
            const maxVal = d[`max_${vf}`] as number | undefined;
            return fill(
              getFeatureFillColor(
                val as number | null | undefined,
                minVal,
                maxVal,
                clr,
                fr,
                0,
                densityGradientRef.current,
                dark,
                255,
                enumCountRef.current,
                enumPaletteRef.current,
                featureGradientRef.current
              )
            );
          }
        }

        const cr = countRangeRef.current;
        const c = d.count as number;
        const t = (c - cr.min) / (cr.max - cr.min);
        return fill(
          getFeatureFillColor(
            null,
            undefined,
            undefined,
            null,
            null,
            t,
            densityGradientRef.current,
            dark,
            255
          )
        );
      },
      getLineColor: (d) => {
        if (d.h3 === selectedHexagonSelectionRef.current)
          return [29, 228, 195, 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 === selectedHexagonSelectionRef.current) return 4;
        if (d.h3 === hoveredHexagonIdRef.current) return 2;
        return 0;
      },
      lineWidthUnits: 'pixels',
      updateTriggers: {
        getFillColor: [colorTrigger, data],
        getLineColor: [colorTrigger],
        getLineWidth: [colorTrigger],
        ...(pieProps.updateTriggers || {}),
      },
      extruded: false,
      pickable: true,
      opacity: 1,
      highPrecision: true,
      onClick: handleHexagonClick,
      onHover: handleHexagonHover,
      beforeId: mapDataBeforeId,
      ...pieProps,
    });
  }, [data, colorTrigger, handleHexagonClick, handleHexagonHover, mapDataBeforeId]);

  const postcodeLayer = useMemo(() => {
    const isEnum = enumCountRef.current > 0;
    const distKey = viewFeatureRef.current ? `dist_${viewFeatureRef.current}` : '';

    const ratiosCache = new WeakMap<PostcodeFeature, number[]>();
    const getRatios = (f: PostcodeFeature): number[] => {
      let r = ratiosCache.get(f);
      if (!r) {
        r = distToRatios(f.properties[distKey]);
        ratiosCache.set(f, r);
      }
      return r;
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pieProps: Record<string, any> = isEnum
      ? {
          extensions: [new PieHexExtension(requireEnumPalette(enumPaletteRef.current))],
          getCenter: (f: PostcodeFeature) => f.properties.centroid,
          getRatios0: (f: PostcodeFeature) => {
            const r = getRatios(f);
            return [r[0], r[1], r[2], r[3]];
          },
          getRatios1: (f: PostcodeFeature) => {
            const r = getRatios(f);
            return [r[4], r[5], r[6], r[7]];
          },
          getRatios2: (f: PostcodeFeature) => {
            const r = getRatios(f);
            return [r[8], r[9]];
          },
        }
      : {};
    const pieUpdateTriggers: Record<string, unknown> = isEnum
      ? {
          getCenter: [postcodeColorTrigger, postcodeData],
          getRatios0: [postcodeColorTrigger, postcodeData],
          getRatios1: [postcodeColorTrigger, postcodeData],
          getRatios2: [postcodeColorTrigger, postcodeData],
        }
      : {};

    return new GeoJsonLayer<PostcodeProperties>({
      ...pieProps,
      id: isEnum ? 'postcode-polygons-pie' : 'postcode-polygons',
      data: postcodeData as PostcodeFeature[],
      getFillColor: (f) => {
        const d = f.properties;
        if (d.count <= 0) {
          return [0, 0, 0, 0] as [number, number, number, number];
        }
        const fill = (color: RgbaColor) => withColorOpacity(color, colorOpacityRef.current);
        const dark = isDarkRef.current;
        const vf = viewFeatureRef.current;
        const clr = colorRangeRef.current;
        const fr = filterRangeRef.current;
        const cfm = colorFeatureMetaRef.current;

        if (vf && clr) {
          // Travel time feature: dim postcodes with no data
          if (vf.startsWith('tt_')) {
            const ttVal = d[`avg_${vf}`];
            if (ttVal == null) {
              return fill(dark ? [80, 70, 65, 80] : [128, 128, 128, 80]);
            }
            const ttMin = (d[`min_${vf}`] as number) ?? ttVal;
            const ttMax = (d[`max_${vf}`] as number) ?? ttVal;
            return fill(
              getFeatureFillColor(
                ttVal as number,
                ttMin as number,
                ttMax as number,
                clr,
                fr,
                0,
                densityGradientRef.current,
                dark,
                180,
                0,
                undefined,
                featureGradientRef.current
              )
            );
          }

          // Regular feature (for enum, the extension overrides this color)
          if (cfm) {
            const val = d[`avg_${vf}`] ?? d[`min_${vf}`];
            const minVal = d[`min_${vf}`] as number | undefined;
            const maxVal = d[`max_${vf}`] as number | undefined;
            return fill(
              getFeatureFillColor(
                val as number | null | undefined,
                minVal,
                maxVal,
                clr,
                fr,
                0,
                densityGradientRef.current,
                dark,
                180,
                enumCountRef.current,
                enumPaletteRef.current,
                featureGradientRef.current
              )
            );
          }
        }
        const cr = postcodeCountRangeRef.current;
        const c = d.count;
        const t = (c - cr.min) / (cr.max - cr.min);
        return fill(
          getFeatureFillColor(
            null,
            undefined,
            undefined,
            null,
            null,
            t,
            densityGradientRef.current,
            dark,
            180
          )
        );
      },
      getLineColor: (f) => {
        const pc = f.properties.postcode;
        const dark = isDarkRef.current;
        if (pc === selectedPostcodeIdRef.current)
          return [29, 228, 195, 255] as [number, number, number, number];
        if (pc === hoveredPostcodeRef.current)
          return [29, 228, 195, 200] as [number, number, number, number];
        if (f.properties.count <= 0) return [0, 0, 0, 0] 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 === selectedPostcodeIdRef.current) return 4;
        if (pc === hoveredPostcodeRef.current) return 2;
        if (f.properties.count <= 0) return 0;
        return 1;
      },
      lineWidthUnits: 'pixels',
      updateTriggers: {
        getFillColor: [postcodeColorTrigger],
        getLineColor: [postcodeColorTrigger],
        getLineWidth: [postcodeColorTrigger],
        ...pieUpdateTriggers,
      },
      extruded: false,
      pickable: true,
      onClick: handlePostcodeClick,
      onHover: handlePostcodeHoverCallback,
      // @ts-expect-error beforeId is a MapboxOverlay interleave prop, not typed in LayerProps
      beforeId: mapDataBeforeId,
    });
  }, [
    postcodeData,
    postcodeColorTrigger,
    handlePostcodeClick,
    handlePostcodeHoverCallback,
    mapDataBeforeId,
  ]);

  const labeledPostcodeData = useMemo(
    () => postcodeData.filter((feature) => feature.properties.count > 0),
    [postcodeData]
  );

  const postcodeLabelsLayer = useMemo(
    () =>
      new TextLayer<PostcodeFeature>({
        id: 'postcode-labels',
        data: labeledPostcodeData,
        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,
      }),
    [labeledPostcodeData, theme]
  );

  // Marching ants highlight layer for selected hexagon or postcode
  const marchingAntsLayer = useMemo(() => {
    let geometry: PostcodeGeometry | null = null;
    if (selectedPostcodeGeometry) {
      geometry = selectedPostcodeGeometry;
    } else if (selectedHexagonId && isValidCell(selectedHexagonId)) {
      const boundary = cellToBoundary(selectedHexagonId, true);
      geometry = { type: 'Polygon', coordinates: [boundary] };
    }
    if (!geometry) return null;
    return new GeoJsonLayer({
      id: 'marching-ants',
      data: [
        {
          type: 'Feature' as const,
          geometry,
          properties: {},
        },
      ],
      filled: false,
      stroked: true,
      getLineColor: [29, 228, 195, 255],
      getLineWidth: 3,
      lineWidthUnits: 'pixels' as const,
      pickable: false,
      parameters: { depthTest: false },
      marchTime,
      extensions: [new MarchingAntsExtension()],
    });
  }, [selectedPostcodeGeometry, selectedHexagonId, marchTime]);

  const currentLocationLayer = useMemo(() => {
    if (!currentLocation) return null;
    return new ScatterplotLayer<{ lat: number; lng: number; kind: 'ring' | 'dot' }>({
      id: 'current-location-dot',
      data: [
        { ...currentLocation, kind: 'ring' },
        { ...currentLocation, kind: 'dot' },
      ],
      getPosition: (d) => [d.lng, d.lat],
      getRadius: (d) => (d.kind === 'ring' ? 16 : 5),
      radiusUnits: 'pixels',
      getFillColor: (d) => (d.kind === 'ring' ? [20, 184, 166, 45] : [220, 38, 38, 255]),
      getLineColor: (d) => (d.kind === 'ring' ? [20, 184, 166, 240] : [255, 255, 255, 240]),
      getLineWidth: 2,
      lineWidthUnits: 'pixels',
      stroked: true,
      pickable: false,
    });
  }, [currentLocation]);

  const layers = useMemo(() => {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const baseLayers: any[] = [];
    if (usePostcodeView) {
      baseLayers.push(postcodeLayer);
      if (zoom >= 16) baseLayers.push(postcodeLabelsLayer);
      baseLayers.push(...poiLayers);
    } else {
      baseLayers.push(hexLayer);
      baseLayers.push(...poiLayers);
    }
    if (marchingAntsLayer) baseLayers.push(marchingAntsLayer);
    if (currentLocationLayer) baseLayers.push(currentLocationLayer);
    if (listingLayers.length > 0) baseLayers.push(...listingLayers);
    return baseLayers;
  }, [
    usePostcodeView,
    zoom,
    hexLayer,
    postcodeLayer,
    postcodeLabelsLayer,
    poiLayers,
    marchingAntsLayer,
    currentLocationLayer,
    listingLayers,
  ]);

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

  return {
    layers,
    visiblePois,
    popupInfo,
    clearPopupInfo,
    listingPopup,
    clearListingPopup,
    hoverPosition,
    countRange,
    postcodeCountRange,
    colorFeatureMeta,
    handleMouseLeave,
    hoveredPostcode,
  };
}
