import { useCallback, useRef, useEffect, useState, useMemo } 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 } 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';

interface MapProps {
  data: HexagonData[];
  pois: POI[];
  onViewChange: (params: ViewChangeParams) => void;
  activeFeature: string | null;
  dragValue: [number, number] | null;
  features: FeatureMeta[];
  selectedHexagonId: string | null;
  onHexagonClick: (h3: string) => void;
}

// Twemoji CDN base URL
const TWEMOJI_BASE = 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/';

// Convert emoji to Twemoji URL
function emojiToTwemojiUrl(emoji: string): string {
  // Convert emoji to Unicode codepoint hex
  const codePoint = emoji.codePointAt(0);
  if (!codePoint) return `${TWEMOJI_BASE}1f4cd.png`; // Default pin
  const hex = codePoint.toString(16);
  return `${TWEMOJI_BASE}${hex}.png`;
}

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

const MAP_STYLE = 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json';

// Gradient stops for normalized [0,1] values
const GRADIENT: { t: number; color: [number, number, number] }[] = [
  { t: 0, color: [46, 204, 113] }, // Green
  { t: 0.33, color: [241, 196, 15] }, // Yellow
  { t: 0.66, color: [231, 76, 60] }, // Red
  { t: 1, color: [142, 68, 173] }, // Purple
];

function normalizedToColor(t: number): [number, number, number] {
  if (t <= 0) return GRADIENT[0].color;
  if (t >= 1) return GRADIENT[GRADIENT.length - 1].color;

  for (let i = 0; i < GRADIENT.length - 1; i++) {
    const lo = GRADIENT[i];
    const hi = GRADIENT[i + 1];
    if (t >= lo.t && t <= hi.t) {
      const frac = (t - lo.t) / (hi.t - lo.t);
      return [
        Math.round(lo.color[0] + (hi.color[0] - lo.color[0]) * frac),
        Math.round(lo.color[1] + (hi.color[1] - lo.color[1]) * frac),
        Math.round(lo.color[2] + (hi.color[2] - lo.color[2]) * frac),
      ];
    }
  }
  return GRADIENT[GRADIENT.length - 1].color;
}

function zoomToResolution(zoom: number): number {
  if (zoom < 7) return 7;
  if (zoom < 9.5) return 8;
  if (zoom < 11) return 9;
  if (zoom < 13) return 10;
  return 11;
}

function getBoundsFromViewState(viewState: ViewState, width: number, height: number): Bounds {
  const { longitude, latitude, zoom } = viewState;

  // Clamp latitude to valid Mercator range to avoid math errors
  const clampedLat = Math.max(-85, Math.min(85, latitude));

  // Web Mercator projection math
  const TILE_SIZE = 256;
  const scale = Math.pow(2, zoom);
  const worldSize = TILE_SIZE * scale;

  // Longitude is linear
  const degreesPerPixelLng = 360 / worldSize;
  const halfWidthDeg = (width / 2) * degreesPerPixelLng;

  // Latitude uses Mercator projection (non-linear)
  const latRad = (clampedLat * Math.PI) / 180;
  const mercatorY = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2;
  const centerPixelY = mercatorY * worldSize;

  const topPixelY = centerPixelY - height / 2;
  const bottomPixelY = centerPixelY + height / 2;

  // Convert pixel Y back to latitude
  const pixelYToLat = (pixelY: number): number => {
    const mercY = Math.max(0.001, Math.min(0.999, pixelY / worldSize));
    const latRadians = Math.atan(Math.sinh(Math.PI * (1 - 2 * mercY)));
    return (latRadians * 180) / Math.PI;
  };

  const north = Math.min(85, pixelYToLat(topPixelY));
  const south = Math.max(-85, pixelYToLat(bottomPixelY));
  const west = Math.max(-180, longitude - halfWidthDeg);
  const east = Math.min(180, longitude + halfWidthDeg);

  return { south, west, north, east };
}

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

// First label layer in the Carto Positron style — hexagons render below this
const LABEL_LAYER_ID = 'waterway_label';

function DeckOverlay({
  layers,
  getTooltip,
}: {
  layers: (H3HexagonLayer<HexagonData> | IconLayer<POI>)[];
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  getTooltip: any;
}) {
  const overlay = useControl(() => new MapboxOverlay({ interleaved: true }));
  overlay.setProps({ layers, getTooltip });
  return null;
}

// Sequential blue scale for count-based coloring
function countToColor(t: number): [number, number, number] {
  // light blue (209, 226, 243) -> dark blue (33, 102, 172)
  const r = Math.round(209 + (33 - 209) * t);
  const g = Math.round(226 + (102 - 226) * t);
  const b = Math.round(243 + (172 - 243) * t);
  return [r, g, b];
}

export default function Map({
  data,
  pois,
  onViewChange,
  activeFeature,
  dragValue,
  features,
  selectedHexagonId,
  onHexagonClick,
}: MapProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [viewState, setViewState] = useState<ViewState>(INITIAL_VIEW);
  const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 });

  // Track container dimensions with ResizeObserver
  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();
  }, []);

  // Notify parent when view or dimensions change
  useEffect(() => {
    if (dimensions.width === 0 || dimensions.height === 0) return;

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

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

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

  // Make place labels more legible over the colored hexagons
  const handleMapLoad = useCallback(
    (evt: { target: MapRef['getMap'] extends () => infer M ? M : never }) => {
      const map = evt.target;
      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');
      }
    },
    []
  );

  // Popup state for POI hover
  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);
    }
  }, []);

  // Compute count range for count-based coloring
  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]);

  // Determine color mode
  const colorFeatureMeta = activeFeature
    ? features.find((f) => f.name === activeFeature) || null
    : null;

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

  const layers = useMemo(
    () => [
      new H3HexagonLayer<HexagonData>({
        id: 'h3-hexagons',
        data,
        getHexagon: (d) => d.h3,
        getFillColor: (d) => {
          if (activeFeature && dragValue && colorFeatureMeta) {
            // Drag mode: color by feature value using gradient
            const val = d[`min_${activeFeature}`];
            if (val == null) return [128, 128, 128] as [number, number, number];
            const range = dragValue[1] - dragValue[0];
            if (range === 0) return GRADIENT[0].color;
            const t = ((val as number) - dragValue[0]) / range;
            return normalizedToColor(Math.max(0, Math.min(1, t)));
          }
          // Normal mode: color by count using blue scale
          const c = d.count as number;
          const t = (c - countRange.min) / (countRange.max - countRange.min);
          return countToColor(Math.max(0, Math.min(1, t)));
        },
        getLineColor: (d) =>
          (d.h3 === selectedHexagonId ? [255, 255, 255, 255] : [0, 0, 0, 0]) as [
            number,
            number,
            number,
            number,
          ],
        getLineWidth: (d) => (d.h3 === selectedHexagonId ? 2 : 0),
        lineWidthUnits: 'pixels',
        updateTriggers: {
          getFillColor: [activeFeature, dragValue, countRange, colorFeatureMeta],
          getLineColor: [selectedHexagonId],
          getLineWidth: [selectedHexagonId],
        },
        extruded: false,
        pickable: true,
        opacity: 0.5,
        highPrecision: true,
        onClick: handleHexagonClick,
        // @ts-expect-error beforeId is a MapboxOverlay interleave prop, not typed in LayerProps
        beforeId: LABEL_LAYER_ID,
      }),
      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: handlePoiHover,
      }),
    ],
    [
      data,
      pois,
      handlePoiHover,
      handleHexagonClick,
      activeFeature,
      dragValue,
      countRange,
      colorFeatureMeta,
      selectedHexagonId,
    ]
  );

  const getTooltip = useCallback(
    ({ object }: { object?: HexagonData }) => {
      if (!object || !('h3' in object)) return null;

      const hex = object;
      const lines: string[] = [];
      lines.push(`<strong>${(hex.count as number).toLocaleString()} properties</strong>`);

      for (const f of features) {
        const minVal = hex[`min_${f.name}`];
        const maxVal = hex[`max_${f.name}`];
        if (minVal != null && maxVal != null) {
          const minStr =
            typeof minVal === 'number'
              ? minVal.toLocaleString(undefined, { maximumFractionDigits: 1 })
              : String(minVal);
          const maxStr =
            typeof maxVal === 'number'
              ? maxVal.toLocaleString(undefined, { maximumFractionDigits: 1 })
              : String(maxVal);
          const highlight = f.name === activeFeature ? 'font-weight: bold;' : '';
          lines.push(`<div style="${highlight}">${f.label}: ${minStr} - ${maxStr}</div>`);
        }
      }

      return {
        html: `<div style="padding: 8px; font-size: 12px;">${lines.join('')}</div>`,
        style: {
          backgroundColor: 'white',
          borderRadius: '4px',
          boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
        },
      };
    },
    [features, activeFeature]
  );

  return (
    <div className="flex-1 h-full relative" ref={containerRef}>
      <MapGL
        {...viewState}
        onMove={handleMove}
        onLoad={handleMapLoad as never}
        mapStyle={MAP_STYLE}
        style={{ width: '100%', height: '100%' }}
        attributionControl={false}
      >
        <DeckOverlay layers={layers} getTooltip={getTooltip as never} />
      </MapGL>
      {popupInfo && (
        <div
          className="absolute pointer-events-none bg-white rounded shadow-lg p-2 text-sm"
          style={{
            left: popupInfo.x,
            top: popupInfo.y - 40,
            transform: 'translateX(-50%)',
            zIndex: 9999,
          }}
        >
          <strong>{popupInfo.name}</strong>
          <div className="text-gray-500 text-xs">{popupInfo.category}</div>
        </div>
      )}
    </div>
  );
}
