import { useCallback, useEffect, useRef } from "react";
import type { RefObject } from "react";

import L, {
  type LeafletMouseEvent,
  type Map as LeafletMap,
  type LayerGroup,
  type TileLayer,
  type TileLayerOptions,
} from "leaflet";
import "leaflet.heat";

import type { SignalDensityCell, Point } from "@/types/api";

type HeatPoint = [number, number, number?];

type LeafletHeatLayer = L.Layer & {
  setLatLngs(points: HeatPoint[]): LeafletHeatLayer;
  getBounds?: () => L.LatLngBounds;
};

type LeafletWithHeat = typeof L & {
  heatLayer?: (points: HeatPoint[], options?: Record<string, unknown>) => LeafletHeatLayer;
};

interface UseLeafletHeatmapParams {
  heatCells: SignalDensityCell[];
  userLocation: Point | null;
  onRequestSpot?: (position: Point) => void;
  tileProvider: TileProvider;
}

interface UseLeafletHeatmapResult {
  mapContainerRef: RefObject<HTMLDivElement | null>;
  focusOn: (position: Point, zoom?: number) => void;
  fitToHeat: () => void;
  map: LeafletMap | null;
}

type TileSource = {
  readonly url: string;
  readonly attribution: string;
  readonly options?: TileLayerOptions;
};

const TILE_SOURCES = {
  openstreetmap: {
    url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
    attribution: "© OpenStreetMap contributors",
  },
  mapbox: {
    url: "https://api.mapbox.com/styles/v1/mapbox/navigation-day-v1/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoiMWNhbnNhIiwiYSI6ImNsdzZ5cHp3bTFheWUydHJ6dHA4empteWEifQ.a3bODguIOY5HqhsVIvW48Q",
    attribution: "© Mapbox, © OpenStreetMap contributors",
    options: {
      tileSize: 512,
      zoomOffset: -1,
    } satisfies TileLayerOptions,
  },
} as const satisfies Record<string, TileSource>;

export type TileProvider = keyof typeof TILE_SOURCES;

const INITIAL_VIEW: Point = { lat: 20, lng: 0 };
const DEFAULT_ZOOM = 17;

export function useLeafletHeatmap({
  heatCells,
  userLocation,
  onRequestSpot,
  tileProvider,
}: UseLeafletHeatmapParams): UseLeafletHeatmapResult {
  const mapRef = useRef<LeafletMap | null>(null);
  const heatLayerRef = useRef<LeafletHeatLayer | null>(null);
  const userLayerRef = useRef<LayerGroup | null>(null);
  const tileLayerRef = useRef<TileLayer | null>(null);
  const hasCenteredOnUserRef = useRef(false);
  const mapContainerRef = useRef<HTMLDivElement | null>(null);
  const onRequestSpotRef = useRef(onRequestSpot);
  const tileProviderRef = useRef<TileProvider>(tileProvider);

  const createTileLayer = useCallback((provider: TileProvider) => {
    const leaflet = L as LeafletWithHeat;
    const source: TileSource = TILE_SOURCES[provider];
    const options = source.options ?? {};
    return leaflet.tileLayer(source.url, {
      attribution: source.attribution,
      crossOrigin: true,
      maxZoom: 20,
      ...options,
    });
  }, []);

  const initialiseMap = useCallback(() => {
    if (mapRef.current || !mapContainerRef.current) {
      return;
    }

    const leaflet = L as LeafletWithHeat;
    const container = mapContainerRef.current;

    const map = leaflet
      .map(container, {
        worldCopyJump: true,
        minZoom: 4,
        zoomControl: false,
        attributionControl: true,
      })
      .setView([INITIAL_VIEW.lat, INITIAL_VIEW.lng], DEFAULT_ZOOM);

    const tileLayer = createTileLayer(tileProviderRef.current);
    tileLayer.addTo(map);

    const heatLayer =
      typeof leaflet.heatLayer === "function"
        ? leaflet.heatLayer([], {
            radius: 32,
            blur: 24,
            maxZoom: 12,
            gradient: {
              0.2: "#38bdf8",
              0.4: "#0ea5e9",
              0.6: "#fbbf24",
              0.8: "#fb923c",
              1: "#ef4444",
            },
          })
        : null;

    const userLayer = leaflet.layerGroup().addTo(map);

    if (heatLayer) {
      heatLayer.addTo(map);
    }

    const handleClick = (event: LeafletMouseEvent) => {
      const { lat, lng } = event.latlng;
      if (typeof lat === "number" && typeof lng === "number") {
        onRequestSpotRef.current?.({ lat, lng });
      }
    };

    map.on("click", handleClick);

    map.whenReady(() => {
      requestAnimationFrame(() => {
        map.invalidateSize();
      });
    });

    const onResize = () => {
      requestAnimationFrame(() => {
        map.invalidateSize();
      });
    };

    window.addEventListener("resize", onResize);

    mapRef.current = map;
    heatLayerRef.current = heatLayer;
    userLayerRef.current = userLayer;
    tileLayerRef.current = tileLayer;

    return () => {
      map.off("click", handleClick);
      window.removeEventListener("resize", onResize);
      // Be explicit when tearing down layers to avoid animation-queued redraws
      try {
        const hl = heatLayerRef.current ?? heatLayer;
        if (hl) {
          // Cancel any pending requestAnimationFrame used internally by the heat layer
          const anyL = L as unknown as { Util: { cancelAnimFrame: (id: any) => void } };
          const anyHl = hl as any;
          if (anyHl._frame) {
            anyL.Util.cancelAnimFrame(anyHl._frame);
          }
          if (typeof anyHl.remove === "function") {
            anyHl.remove();
          } else if (typeof anyHl.removeFrom === "function") {
            anyHl.removeFrom(map);
          }
        }
      } catch {
        // noop
      }

      try {
        userLayer.remove();
      } catch {
        // noop
      }

      try {
        tileLayer.remove();
      } catch {
        // noop
      }

      map.remove();
      mapRef.current = null;
      heatLayerRef.current = null;
      userLayerRef.current = null;
      tileLayerRef.current = null;
      hasCenteredOnUserRef.current = false;
    };
  }, [createTileLayer]);

  useEffect(() => {
    onRequestSpotRef.current = onRequestSpot;
  }, [onRequestSpot]);

  useEffect(() => {
    const cleanup = initialiseMap();

    return () => {
      if (typeof cleanup === "function") {
        cleanup();
      }
    };
  }, [initialiseMap]);

  useEffect(() => {
    tileProviderRef.current = tileProvider;
    const map = mapRef.current;
    if (!map) {
      return;
    }

    const nextLayer = createTileLayer(tileProvider);
    const currentLayer = tileLayerRef.current;

    if (currentLayer) {
      currentLayer.removeFrom(map);
    }

    nextLayer.addTo(map);
    tileLayerRef.current = nextLayer;
  }, [createTileLayer, tileProvider]);

  useEffect(() => {
    const heatLayer = heatLayerRef.current as any;
    if (!heatLayer || !heatLayer._map) {
      return;
    }

    if (!heatCells.length) {
      heatLayer.setLatLngs([]);
      return;
    }

    const maxIntensity = Math.max(...heatCells.map(cell => cell.intensity)) || 1;
    const heatPoints: HeatPoint[] = heatCells.map(cell => [
      cell.lat,
      cell.lng,
      Math.max(0.2, cell.intensity / maxIntensity),
    ]);

    heatLayer.setLatLngs(heatPoints);
  }, [heatCells]);

  useEffect(() => {
    const userLayer = userLayerRef.current;
    const map = mapRef.current;
    if (!userLayer || !map) {
      return;
    }

    userLayer.clearLayers();

    if (!userLocation) {
      hasCenteredOnUserRef.current = false;
      return;
    }

    L.circleMarker([userLocation.lat, userLocation.lng], {
      radius: 7,
      color: "#38bdf8",
      weight: 3,
      opacity: 0.9,
      fillColor: "#38bdf8",
      fillOpacity: 0.35,
    }).addTo(userLayer);

    if (!hasCenteredOnUserRef.current) {
      map.setView([userLocation.lat, userLocation.lng], 17, { animate: true });
      hasCenteredOnUserRef.current = true;
    }
  }, [userLocation]);

  const focusOn = useCallback((position: Point, zoom = 17) => {
    const map = mapRef.current;
    if (!map) {
      return;
    }
    map.setView([position.lat, position.lng], zoom, { animate: true });
  }, []);

  const fitToHeat = useCallback(() => {
    const map = mapRef.current;
    const heatLayer = heatLayerRef.current;
    if (!map || !heatLayer) {
      return;
    }
    const bounds = heatLayer.getBounds?.();
    if (bounds && bounds.isValid()) {
      map.fitBounds(bounds.pad(0.25), { animate: true });
    }
  }, []);

  return {
    mapContainerRef,
    focusOn,
    fitToHeat,
    map: mapRef.current,
  };
}
