import { useEffect, useRef } from 'react';
import L from 'leaflet';
import 'leaflet.heat';
import type { Transaction } from '../data/mockData';

// Leaflet default marker fix (Vite asset pipeline)
import iconUrl from 'leaflet/dist/images/marker-icon.png';
import iconShadowUrl from 'leaflet/dist/images/marker-shadow.png';
L.Icon.Default.mergeOptions({ iconUrl, shadowUrl: iconShadowUrl });

interface HeatMapProps {
  transactions: Transaction[];
}

const HEAT_OPTIONS: L.HeatMapOptions = {
  radius: 30,
  blur: 22,
  maxZoom: 12,
  max: 1.0,
  minOpacity: 0.25,
  gradient: {
    0.0: '#0d0221',
    0.2: '#1a0a4a',
    0.4: '#3a0e82',
    0.55: '#7b1fa2',
    0.7: '#e65100',
    0.85: '#ff8f00',
    1.0: '#ffd740',
  },
};

export function HeatMap({ transactions }: HeatMapProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const mapRef = useRef<L.Map | null>(null);
  const heatRef = useRef<L.HeatLayer | null>(null);
  const overlayRef = useRef<HTMLImageElement | null>(null);

  // Initialize map once
  useEffect(() => {
    if (!containerRef.current || mapRef.current) return;

    const map = L.map(containerRef.current, {
      center: [46.8, 2.35],
      zoom: 6,
      zoomControl: false,
      attributionControl: true,
    });

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
      maxZoom: 18,
    }).addTo(map);

    L.control.zoom({ position: 'bottomright' }).addTo(map);

    const heat = L.heatLayer([], HEAT_OPTIONS).addTo(map);

    mapRef.current = map;
    heatRef.current = heat;

    return () => {
      map.remove();
      mapRef.current = null;
      heatRef.current = null;
    };
  }, []);

  // True crossfade: old frame (overlay) fades out WHILE new frame (canvas) fades in simultaneously
  useEffect(() => {
    if (!heatRef.current || !mapRef.current) return;

    const canvas = (heatRef.current as unknown as { _canvas?: HTMLCanvasElement })._canvas;
    const overlay = overlayRef.current;

    const update = () => {
      const maxAmount = Math.max(...transactions.map((t) => t.amount), 1);
      const points: L.HeatLatLngTuple[] = transactions.map((tx) => [
        tx.lat,
        tx.lng,
        Math.min(Math.log1p(tx.amount) / Math.log1p(maxAmount), 1),
      ]);
      try {
        heatRef.current?.setLatLngs(points);
      } catch {
        // map was torn down (React StrictMode double-invoke), ignore
      }
    };

    if (!canvas || !overlay) {
      update();
      return;
    }

    // 1. Capture old frame into overlay
    try {
      overlay.src = canvas.toDataURL();
    } catch {
      update();
      return;
    }

    // 2. Snap overlay to visible, cancel any running transition
    overlay.style.transition = 'none';
    overlay.style.opacity = '1';

    // 3. Hide canvas (new frame will fade in from here)
    canvas.style.transition = 'none';
    canvas.style.opacity = '0';

    // flush both
    void overlay.offsetWidth;

    // 4. Draw new data onto canvas (invisible at opacity 0)
    update();

    // 5. Simultaneously: canvas fades in, overlay fades out → true crossfade
    // Both RAF ids must be cancelled on cleanup to avoid double-transition
    let raf2 = 0;
    const raf1 = requestAnimationFrame(() => {
      raf2 = requestAnimationFrame(() => {
        const DURATION = '0.55s ease-in-out';
        canvas.style.transition = `opacity ${DURATION}`;
        canvas.style.opacity = '1';
        overlay.style.transition = `opacity ${DURATION}`;
        overlay.style.opacity = '0';
      });
    });

    return () => { cancelAnimationFrame(raf1); cancelAnimationFrame(raf2); };
  }, [transactions]);

  return (
    <div className="w-full h-full relative" style={{ minHeight: 0 }}>
      <div ref={containerRef} className="absolute inset-0" />
      <img
        ref={overlayRef}
        alt=""
        className="absolute inset-0 w-full h-full pointer-events-none"
        style={{ opacity: 0, zIndex: 500 }}
      />
    </div>
  );
}
