import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
  MapContainer,
  TileLayer,
  Marker,
  Polyline,
  Rectangle,
  useMap,
  useMapEvents,
  Pane,
} from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import type { ProcessNode } from '../../types/process';
import { extractStreamInfo } from '../../utils/streamInfo';
import './MapViewer.css';

// Fix missing Leaflet default marker icons
import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png';
import markerIcon from 'leaflet/dist/images/marker-icon.png';
import markerShadow from 'leaflet/dist/images/marker-shadow.png';

// @ts-ignore
delete (L.Icon.Default.prototype as any)._getIconUrl;
L.Icon.Default.mergeOptions({
  iconRetinaUrl: markerIcon2x,
  iconUrl: markerIcon,
  shadowUrl: markerShadow,
});

interface GroupCoords {
  lat?: string | number | null;
  lon?: string | number | null;
  hours?: string;
  box_scale?: string | number;
}

interface Props {
  center: [number, number];
  zoom: number;
  locked: boolean;
  processes: ProcessNode[];
  groups: number[][];
  groupNames: string[];
  groupCoordinates: Record<string, GroupCoords>;
  baseTile: string;
  onClick: (lat: number, lon: number) => void;
  onMoveEnd: (center: [number, number], zoom: number) => void;
  subprocessMapExpanded: Record<number, boolean>;
  childMapExpanded: Record<number, boolean>;
  onProcessesChange?: (processes: ProcessNode[]) => void;
  onGroupCoordinatesChange?: (coords: Record<string, GroupCoords>) => void;
  onElementDoubleClick?: (type: 'group' | 'sub' | 'child' | 'stream', id: any, subId?: any) => void;
  height?: string | number;

  // Selection
  selectedStreams?: Record<string, boolean>;
  onProcessesSelect?: (pIdxs: number[], val: boolean) => void;
  onSelectionToggle?: (pIdx: number, si: number) => void;
  selectionActive?: boolean;
  className?: string;
  allowMultiMove?: boolean;
}

/** 
 * BoxSelector: Handles Click-and-Drag area selection on the map.
 * Re-implemented with direct DOM events for maximum reliability on locked maps.
 */
function BoxSelector({ onSelect, active }: {
  onSelect: (bounds: L.LatLngBounds) => void,
  active: boolean
}) {
  const map = useMap();
  const [visualBounds, setVisualBounds] = useState<L.LatLngBounds | null>(null);
  const startLatLngRef = useRef<L.LatLng | null>(null);

  useEffect(() => {
    if (!active) {
      setVisualBounds(null);
      startLatLngRef.current = null;
      return;
    }

    const container = map.getContainer();

    const onDown = (e: MouseEvent) => {
      // 1. Only left click
      if (e.button !== 0) return;

      // 2. Ignore UI
      const target = e.target as HTMLElement;
      if (target.closest('.leaflet-control') || target.closest('.pa-map-toolbar') || target.closest('.leaflet-marker-icon')) {
        return;
      }

      // 3. Start selection
      const latlng = map.mouseEventToLatLng(e);
      startLatLngRef.current = latlng;
      setVisualBounds(null);

      // Prevent map panning
      map.dragging.disable();
    };

    const onMove = (e: MouseEvent) => {
      if (!startLatLngRef.current) return;

      const currentLatLng = map.mouseEventToLatLng(e);
      const bounds = L.latLngBounds(startLatLngRef.current, currentLatLng);
      setVisualBounds(bounds);
    };

    const onUp = (e: MouseEvent) => {
      if (!startLatLngRef.current) {
        map.dragging.enable();
        return;
      }

      const currentLatLng = map.mouseEventToLatLng(e);
      const bounds = L.latLngBounds(startLatLngRef.current, currentLatLng);

      // Clean up state immediately
      startLatLngRef.current = null;
      setVisualBounds(null);
      map.dragging.enable();

      // Trigger selection if meaningful distance
      if (bounds.getNorthEast().distanceTo(bounds.getSouthWest()) > 1.0) {
        onSelect(bounds);
      }
    };

    L.DomEvent.on(container, 'mousedown', onDown as any);
    L.DomEvent.on(window as any, 'mousemove', onMove as any);
    L.DomEvent.on(window as any, 'mouseup', onUp as any);

    return () => {
      L.DomEvent.off(container, 'mousedown', onDown as any);
      L.DomEvent.off(window as any, 'mousemove', onMove as any);
      L.DomEvent.off(window as any, 'mouseup', onUp as any);
      map.dragging.enable();
    };
  }, [active, map, onSelect]);

  if (!visualBounds) return null;
  return (
    <Rectangle
      bounds={visualBounds}
      pathOptions={{ color: '#3498db', weight: 2, dashArray: '6, 8', fillOpacity: 0.15, interactive: false }}
    />
  );
}

const TILE_URLS: Record<string, string> = {
  OpenStreetMap: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
  Positron:
    'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
  Satellite:
    'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
};

function createDivIcon(
  _emoji: string,
  label: string,
  _color: string,
  scale: number = 1.0,
  isSelected: boolean = true,
  isMultiSelected: boolean = false,
  markerType: string = '',
  markerId: string = ''
) {
  const fs = Math.max(10, Math.round(10 * scale));
  const px = Math.max(4, Math.round(8 * scale));
  const py = Math.max(2, Math.round(4 * scale));
  const opacity = isSelected ? 1.0 : 0.4;

  const borderColor = isMultiSelected ? 'var(--warning)' : (isSelected ? '#1b5e20' : '#1b5e20');
  const bg = isMultiSelected ? 'var(--brand-magenta)' : (isSelected ? '#2e7d32' : '#1b5e20');
  const borderWeight = isMultiSelected ? '3px' : '1px';
  const textColor = '#ffffff'; // Always white for green markers for best contrast

  const avgCharWidth = fs * 0.62;
  const width = Math.round(Math.max(20, (label.length * avgCharWidth) + (px * 2) + 2));
  const height = Math.round(fs + (py * 2) + 2);

  const dataAttrs = markerType ? `data-marker-type="${markerType}" data-marker-id="${markerId}"` : '';

  return L.divIcon({
    className: 'custom-marker',
    html: `<div class="draggable-marker" ${dataAttrs} style="background:${bg};color:${textColor};padding:${py}px ${px}px;border-radius:4px;font-size:${fs}px;font-weight:600;white-space:nowrap;width:${width}px;height:${height}px;text-align:center;display:flex;align-items:center;justify-content:center;border:${borderWeight} solid ${borderColor};box-shadow:var(--shadow-sm);opacity:${opacity};transition:background 0.23s,opacity 0.23s;cursor:grab;user-select:none">
      ${label}
    </div>`,
    iconSize: [width, height],
    iconAnchor: [width / 2, height / 2],
  });
}

function MapController({ center, zoom, locked }: { center: [number, number]; zoom: number; locked: boolean }) {
  const map = useMap();
  useEffect(() => {
    if (locked) {
      map.setView(center, zoom, { animate: false });
      map.dragging.disable();
      map.scrollWheelZoom.disable();
      map.doubleClickZoom.disable();
      map.boxZoom.disable();
      map.keyboard.disable();
      if ((map as any).touchZoom) (map as any).touchZoom.disable();
      if ((map as any).tap) (map as any).tap.disable();
    } else {
      map.dragging.enable();
      map.scrollWheelZoom.enable();
      map.doubleClickZoom.enable();
      map.boxZoom.enable();
      map.keyboard.enable();
      if ((map as any).touchZoom) (map as any).touchZoom.enable();
      if ((map as any).tap) (map as any).tap.enable();
    }
  }, [center, zoom, locked, map]);
  return null;
}

function MapMoveHandler({
  onMoveEnd,
  locked,
}: {
  onMoveEnd: (center: [number, number], zoom: number) => void;
  locked: boolean;
}) {
  useMapEvents({
    moveend(e) {
      if (locked) return;
      const map = e.target;
      const c = map.getCenter();
      onMoveEnd([c.lat, c.lng], map.getZoom());
    },
  });
  return null;
}

function MapFullscreenResizer({ active }: { active: boolean }) {
  const map = useMap();
  useEffect(() => {
    map.invalidateSize();
    const delays = [50, 200, 500];
    const timers = delays.map(d => setTimeout(() => map.invalidateSize(), d));
    return () => timers.forEach(t => clearTimeout(t));
  }, [active, map]);
  return null;
}

function MapClickHandler({ onClick }: { onClick?: (lat: number, lon: number) => void }) {
  useMapEvents({
    click(e) {
      if (onClick) onClick(e.latlng.lat, e.latlng.lng);
    },
  });
  return null;
}

/**
 * Called once on mount: invalidates the Leaflet container size, waits a tick
 * for Leaflet to recalculate its internal pixel grid, then snaps the view to
 * the correct center/zoom. invalidateSize and setView MUST be in separate
 * timeouts — calling them together means setView runs before Leaflet has
 * finished updating its size math, causing the "cut-off corner" bug.
 */
function MapMountFitter({
  center,
  zoom,
}: {
  center: [number, number];
  zoom: number;
}) {
  const map = useMap();
  useEffect(() => {
    const timers: ReturnType<typeof setTimeout>[] = [];
    // Step 1: force Leaflet to measure the real container size
    const invalidate = () => map.invalidateSize({ animate: false });
    // Step 2: after size is known, snap to correct center+zoom
    const refit = () => map.setView(center, zoom, { animate: false });

    timers.push(setTimeout(invalidate, 0));
    timers.push(setTimeout(refit, 50));
    timers.push(setTimeout(invalidate, 100));
    timers.push(setTimeout(refit, 150));
    timers.push(setTimeout(invalidate, 300));
    timers.push(setTimeout(refit, 400));

    return () => timers.forEach(t => clearTimeout(t));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  return null;
}

function SubprocessCanvasOverlay({ active }: { active: boolean }) {
  const map = useMap();
  const [bounds, setBounds] = useState<L.LatLngBounds | null>(null);

  useEffect(() => {
    if (active) {
      const tb = map.getBounds();
      const n = tb.getNorth();
      const s = tb.getSouth();
      const e = tb.getEast();
      const w = tb.getWest();

      const latDiff = n - s;
      const lngDiff = e - w;

      // Expand canvas to cover about 90% of the view area (5% margins)
      const newBounds = L.latLngBounds(
        L.latLng(s + latDiff * 0.05, w + lngDiff * 0.05),
        L.latLng(n - latDiff * 0.05, e - lngDiff * 0.05)
      );
      setBounds(newBounds);
    } else {
      setBounds(null);
    }
  }, [active, map]);

  if (!active || !bounds) return null;

  return (
    <Rectangle
      bounds={bounds}
      pane="canvasPane"
      pathOptions={{ color: 'var(--border)', weight: 1, fillColor: 'var(--surface)', fillOpacity: 0.96 }}
    />
  );
}

// ─── Stream Circles Overlay ─────────────────────────────────────────────────
// Draws SVG bubble circles on the map above each process group, matching the
// Streamlit app: circle SIZE = Q heat power (kW, globally min-max scaled),
// COLOR = continuous blue→red gradient based on global temperature range.

interface StreamCirclesOverlayProps {
  processes: ProcessNode[];
  groups: number[][];
  groupNames: string[];
  groupCoordinates: Record<string, GroupCoords>;
  onElementDoubleClick?: (type: 'group' | 'sub' | 'child' | 'stream', id: any, subId?: any) => void;
  selectedStreams?: Record<string, boolean>;
  onSelectionToggle?: (pIdx: number, si: number) => void;
  locked?: boolean;
  onDragEnd?: (e: L.LeafletEvent, type: 'group' | 'sub' | 'child', id: string) => void;
  renderingMode?: 'default' | 'children';
  childMapExpanded?: Record<number, boolean>;
}

function streamColor(isHot: boolean, t: number, isSelected: boolean): { fill: string; stroke: string } {
  if (!isSelected) {
    return {
      fill: 'rgba(180,180,180,0.3)',
      stroke: 'rgba(150,150,150,0.4)',
    };
  }
  if (isHot) {
    const r = 255;
    const g = Math.round(120 - t * 120);
    const b = Math.round(120 - t * 120);
    return {
      fill: `rgba(${r},${g},${b},0.88)`,
      stroke: `rgba(${Math.round(r * 0.6)},0,0,0.95)`,
    };
  } else {
    const r = Math.round(100 - t * 80);
    const g = Math.round(150 - t * 120);
    const b = 255;
    return {
      fill: `rgba(${r},${g},${b},0.88)`,
      stroke: `rgba(0,${Math.round(g * 0.4)},${Math.round(b * 0.6)},0.95)`,
    };
  }
}

function StreamCirclesOverlay({
  processes,
  groups,
  groupNames: _groupNames,
  groupCoordinates,
  onElementDoubleClick,
  selectedStreams,
  onSelectionToggle,
  locked = false,
  onDragEnd,
  renderingMode = 'default',
  childMapExpanded,
}: StreamCirclesOverlayProps) {
  interface StreamBubble {
    name: string;
    subprocess: string;
    tin: number | null;
    tout: number | null;
    Q: number | null;
    pIdx: number;
    ci?: number;
    si: number;
  }
  interface GroupBubbles {
    gIdx: number;
    lat: number;
    lon: number;
    streams: StreamBubble[];
  }

  const groupBubbles = useMemo<GroupBubbles[]>(() => {
    const result: GroupBubbles[] = [];
    if (renderingMode === 'children' && childMapExpanded) {
      processes.forEach((sub, si) => {
        if (!childMapExpanded[si]) return;
        (sub.children || []).forEach((child, ci) => {
          const lat = parseFloat(String(child.lat));
          const lon = parseFloat(String(child.lon));
          if (isNaN(lat) || isNaN(lon)) return;

          const streams: StreamBubble[] = [];
          (child.streams || []).forEach((s, sidx) => {
            const info = extractStreamInfo(s as Record<string, any>);
            streams.push({
              name: s.name || 'Stream',
              subprocess: child.name,
              tin: info.tin, tout: info.tout, Q: info.Q,
              pIdx: si, ci: ci, si: sidx
            });
          });
          if (streams.length > 0) {
            result.push({ gIdx: -1, lat, lon, streams });
          }
        });
      });
    } else {
      groups.forEach((subIdxs, gIdx) => {
        const gCoords = groupCoordinates[gIdx];
        if (!gCoords) return;
        const lat = parseFloat(String(gCoords.lat ?? ''));
        const lon = parseFloat(String(gCoords.lon ?? ''));
        if (isNaN(lat) || isNaN(lon)) return;

        const streams: StreamBubble[] = [];
        subIdxs.forEach((si) => {
          const p = processes[si];
          if (!p) return;
          (p.streams || []).forEach((s, sidx) => {
            const info = extractStreamInfo(s as Record<string, any>);
            streams.push({
              name: s.name || 'Stream',
              subprocess: p.name,
              tin: info.tin, tout: info.tout, Q: info.Q,
              pIdx: si, si: sidx
            });
          });
        });

        if (streams.length > 0) {
          result.push({ gIdx, lat, lon, streams });
        }
      });
    }
    return result;
  }, [processes, groups, groupCoordinates, renderingMode, childMapExpanded]);

  const { qMin, qRange } = useMemo(() => {
    const qs = groupBubbles.flatMap((gb) => gb.streams.map((s) => s.Q ?? 0)).filter((q) => q > 0);
    const mn = qs.length ? Math.min(...qs) : 0;
    const mx = qs.length ? Math.max(...qs) : 1;
    return { qMin: mn, qRange: mx === mn ? 1 : mx - mn };
  }, [groupBubbles]);

  const { hotMin, hotRange, coldMin, coldRange } = useMemo(() => {
    const hotTemps: number[] = [];
    const coldTemps: number[] = [];
    groupBubbles.forEach((gb) => {
      gb.streams.forEach((s) => {
        if (s.tin != null && s.tout != null) {
          const maxT = Math.max(s.tin, s.tout);
          if (s.tin > s.tout) hotTemps.push(maxT);
          else coldTemps.push(maxT);
        }
      });
    });
    const hMin = hotTemps.length ? Math.min(...hotTemps) : 0;
    const hMax = hotTemps.length ? Math.max(...hotTemps) : 200;
    const cMin = coldTemps.length ? Math.min(...coldTemps) : 0;
    const cMax = coldTemps.length ? Math.max(...coldTemps) : 200;
    return {
      hotMin: hMin, hotRange: hMax === hMin ? 1 : hMax - hMin,
      coldMin: cMin, coldRange: cMax === cMin ? 1 : cMax - cMin,
    };
  }, [groupBubbles]);

  const streamColorFor = (tin: number | null, tout: number | null, isSelected: boolean) => {
    if (tin == null || tout == null) return { fill: 'rgba(150,150,150,0.5)', stroke: '#999' };
    const isHot = tin > tout;
    const maxT = Math.max(tin, tout);
    const t = isHot
      ? Math.max(0, Math.min(1, (maxT - hotMin) / hotRange))
      : Math.max(0, Math.min(1, (maxT - coldMin) / coldRange));
    return streamColor(isHot, t, isSelected);
  };

  const R_MIN = 7;
  const R_MAX = 22;
  const getRadius = (Q: number | null) => {
    if (!Q || Q <= 0) return R_MIN;
    return R_MIN + Math.round(((Q - qMin) / qRange) * (R_MAX - R_MIN));
  };

  return (
    <>
      {groupBubbles.map(({ gIdx, lat, lon, streams }) => {
        const SPACING = 6;
        const radii = streams.map((s) => getRadius(s.Q));
        const maxR = Math.max(...radii, R_MIN);
        const svgH = maxR * 2 + 4;

        let cx = (radii[0] ?? R_MIN) + 2;
        const circleParts: string[] = [];
        streams.forEach((s, idx) => {
          const r = radii[idx];
          const isSelected = s.ci !== undefined
            ? selectedStreams?.[`stream_${s.pIdx}_${s.ci}_${s.si}`] !== false
            : selectedStreams?.[`stream_${s.pIdx}_${s.si}`] !== false;

          const { fill, stroke } = streamColorFor(s.tin, s.tout, isSelected);
          circleParts.push(
            `<circle 
               cx="${cx}" 
               cy="${maxR + 2}" 
               r="${r}" 
               fill="${fill}" 
               stroke="${stroke}" 
               stroke-width="${isSelected ? 1.5 : 1}"
               class="stream-bubble"
               data-pidx="${s.pIdx}"
               ${s.ci !== undefined ? `data-cidx="${s.ci}"` : ''}
               data-si="${s.si}"
               style="cursor:pointer; transition:all 0.2s"
             >
               <title>${s.name} (${isSelected ? 'Selected' : 'Deselected'})</title>
             </circle>`
          );
          cx += r + (radii[idx + 1] ?? r) + SPACING;
        });

        const svgW = cx - SPACING;
        const icon = L.divIcon({
          className: 'stream-circles-icon',
          html: `<svg width="${svgW}" height="${svgH}" viewBox="0 0 ${svgW} ${svgH}">${circleParts.join('')}</svg>`,
          iconSize: [svgW, svgH],
          iconAnchor: [svgW / 2, svgH + 10],
        });

        const markerPane = renderingMode === 'children' ? 'expandedBubblesPane' : 'streamBubblesPane';

        return (
          <Marker
            key={`circles-${gIdx}-${lat}-${lon}`}
            position={[lat, lon]}
            icon={icon}
            interactive={true}
            draggable={!locked && renderingMode !== 'children'}
            pane={markerPane}
            eventHandlers={{
              click: (e) => {
                const target = e.originalEvent.target as HTMLElement;
                if (target.tagName === 'circle') {
                  const pid = parseInt(target.getAttribute('data-pidx') || '');
                  const cid = target.getAttribute('data-cidx');
                  const sid = parseInt(target.getAttribute('data-si') || '');

                  if (onSelectionToggle) {
                    onSelectionToggle(pid, sid); // Toggle handles it? Wait, my store might need cIdx
                  } else if (onElementDoubleClick) {
                    onElementDoubleClick('stream', pid, cid !== null ? parseInt(cid) : sid);
                  }
                }
              },
              dragend: (e) => {
                if (onDragEnd && renderingMode !== 'children') onDragEnd(e, 'group', String(gIdx));
              }
            }}
          />
        );
      })}
    </>
  );
}

// ─── Connection Lines ─────────────────────────────────────────────────────────
function ConnectionLines({

  processes,
  groups,
  groupNames,
  groupCoordinates,
  subprocessMapExpanded,
  childMapExpanded,
}: {
  processes: ProcessNode[],
  groups: number[][],
  groupNames: string[],
  groupCoordinates: Record<string, GroupCoords>,
  subprocessMapExpanded: Record<number, boolean>,
  childMapExpanded: Record<number, boolean>
}) {
  const map = useMap();

  const connections = useMemo(() => {
    const result: React.ReactElement[] = [];

    // Determine which nodes are currently visible
    const visibleSubIdxs = new Set<number>();
    const visibleChildIdxs = new Map<number, Set<number>>(); // subIdx -> childIdxs
    const expandedGroups = new Set<number>();

    groups.forEach((subIdxs, gIdx) => {
      if (subprocessMapExpanded[gIdx]) {
        expandedGroups.add(gIdx);
        subIdxs.forEach(si => {
          if (childMapExpanded[si]) {
            const cSet = new Set<number>();
            (processes[si].children || []).forEach((_, ci) => cSet.add(ci));
            visibleChildIdxs.set(si, cSet);
          } else {
            visibleSubIdxs.add(si);
          }
        });
      }
    });



    // Helper to draw a single connection
    const drawConn = (p: ProcessNode, tgt: ProcessNode) => {
      const srcLat = parseFloat(String(p.lat));
      const srcLon = parseFloat(String(p.lon));
      const tLat = parseFloat(String(tgt.lat));
      const tLon = parseFloat(String(tgt.lon));
      if (isNaN(srcLat) || isNaN(srcLon) || isNaN(tLat) || isNaN(tLon)) return null;

      const srcScale = p.box_scale ? parseFloat(String(p.box_scale)) : 1.0;
      const tgtScale = tgt.box_scale ? parseFloat(String(tgt.box_scale)) : 1.0;

      const p1 = map.latLngToContainerPoint([srcLat, srcLon]);
      const p2 = map.latLngToContainerPoint([tLat, tLon]);
      const dist = Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
      if (dist < 40) return null;

      const angleRad = Math.atan2(p2.y - p1.y, p2.x - p1.x);
      const angleDeg = angleRad * (180 / Math.PI);
      const cos = Math.abs(Math.cos(angleRad));
      const sin = Math.abs(Math.sin(angleRad));

      const getOffset = (name: string, scale: number) => {
        const w = (name.length * 6 + 12) * scale / 2;
        const h = (12 * scale + 8) / 2;
        return Math.min(w / (cos || 0.001), h / (sin || 0.001));
      };

      const startOffset = getOffset(p.name, srcScale) + 2;
      const endOffset = getOffset(tgt.name, tgtScale) + 2;
      if (dist <= startOffset + endOffset + 10) return null;

      const startRatio = startOffset / dist;
      const arrowRatio = (dist - endOffset) / dist;
      const lineEndRatio = (dist - endOffset - 12) / dist;

      const pStart = L.point(p1.x + (p2.x - p1.x) * startRatio, p1.y + (p2.y - p1.y) * startRatio);
      const pArrow = L.point(p1.x + (p2.x - p1.x) * arrowRatio, p1.y + (p2.y - p1.y) * arrowRatio);
      const pLineEnd = L.point(p1.x + (p2.x - p1.x) * lineEndRatio, p1.y + (p2.y - p1.y) * lineEndRatio);

      const lStart = map.containerPointToLatLng(pStart);
      const lArrow = map.containerPointToLatLng(pArrow);
      const lEnd = map.containerPointToLatLng(pLineEnd);

      const icon = L.divIcon({
        className: 'custom-arrow',
        html: `<div style="transform: rotate(${angleDeg}deg); display: flex; align-items: center; justify-content: center; width: 20px; height: 20px;">
          <svg width="20" height="20" viewBox="0 0 20 20" overflow="visible" style="display: block;">
            <path d="M2,4 L18,10 L2,16 L6,10 Z" fill="var(--text-main)" />
          </svg>
        </div>`,
        iconSize: [20, 20],
        iconAnchor: [18, 10],
      });

      return (
        <React.Fragment key={`conn-${p.name}-${tgt.name}`}>
          <Polyline positions={[lStart, lEnd]} pathOptions={{ color: 'var(--text-main)', weight: 3, pane: 'activeMarkerPane' }} />
          <Marker position={lArrow} icon={icon} interactive={false} pane="activeMarkerPane" />
        </React.Fragment>
      );
    };

    // 1. Level 0 connections (between Groups)
    if (expandedGroups.size === 0) {
      const groupConns = new Set<string>();
      groups.forEach((subIdxs, gIdx) => {
        subIdxs.forEach(si => {
          const p = processes[si];
          if (!p.next) return;
          const targets = p.next.split(',').map(n => n.trim()).filter(Boolean);
          targets.forEach(tName => {
            let targetGIdx = -1;
            groups.forEach((tSubIdxs, tgIdx) => {
              if (tSubIdxs.some(tsi => processes[tsi].name === tName)) {
                targetGIdx = tgIdx;
              }
            });

            if (targetGIdx !== -1 && targetGIdx !== gIdx) {
              const pairId = `${gIdx}-${targetGIdx}`;
              if (!groupConns.has(pairId)) {
                groupConns.add(pairId);
                const srcGC = groupCoordinates[gIdx];
                const tgtGC = groupCoordinates[targetGIdx];
                if (srcGC && tgtGC) {
                  const srcNode = {
                    name: groupNames[gIdx] || `Process ${gIdx + 1}`,
                    lat: srcGC.lat, lon: srcGC.lon,
                    box_scale: srcGC.box_scale || 1.5
                  } as any;
                  const tgtNode = {
                    name: groupNames[targetGIdx] || `Process ${targetGIdx + 1}`,
                    lat: tgtGC.lat, lon: tgtGC.lon,
                    box_scale: tgtGC.box_scale || 1.5
                  } as any;
                  const el = drawConn(srcNode, tgtNode);
                  if (el) result.push(el);
                }
              }
            }
          });
        });
      });
    }

    // 2. Connections within Level 1 (expanded groups)
    visibleSubIdxs.forEach(si => {
      const p = processes[si];
      if (!p.next) return;
      const targets = p.next.split(',').map(n => n.trim()).filter(Boolean);
      targets.forEach(tName => {
        // Find target among visible siblings
        const tgt = Array.from(visibleSubIdxs).map(idx => processes[idx]).find(pp => pp.name === tName);
        if (tgt) {
          const el = drawConn(p, tgt);
          if (el) result.push(el);
        }
      });
    });

    // 3. Connections within Level 2 (expanded children)
    visibleChildIdxs.forEach((cIdxs, si) => {
      const sub = processes[si];
      cIdxs.forEach(ci => {
        const p = sub.children![ci];
        if (!p.next) return;
        const targets = p.next.split(',').map(n => n.trim()).filter(Boolean);
        targets.forEach(tName => {
          const tgt = sub.children!.find(pp => pp.name === tName);
          if (tgt) {
            const el = drawConn(p, tgt);
            if (el) result.push(el);
          }
        });
      });
    });

    return result;
  }, [processes, groups, subprocessMapExpanded, childMapExpanded, map]);

  return <>{connections}</>;
}

export default function MapViewer({
  center,
  zoom,
  locked,
  processes,
  groups,
  groupNames,
  groupCoordinates,
  baseTile,
  onClick: _onClick,
  onMoveEnd,
  subprocessMapExpanded,
  childMapExpanded,
  onProcessesChange,
  onGroupCoordinatesChange,
  onElementDoubleClick,
  height,
  selectedStreams,
  onProcessesSelect,
  onSelectionToggle,
  selectionActive,
  className,
  allowMultiMove = false,
}: Props) {
  const [isFullscreen, setIsFullscreen] = useState(false);
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
  const lastSelectionTime = useRef(0);

  // Clear selection if we click empty map space (and didn't just finish a box selection)
  const handleMapClick = (lat: number, lon: number) => {
    if (_onClick) _onClick(lat, lon);

    // Tiny 100ms guard for the box selector
    if (Date.now() - lastSelectionTime.current < 100) return;

    if (selectedIds.size > 0) {
      setSelectedIds(new Set());
    }
  };

  useEffect(() => {
    if (isFullscreen) {
      document.body.style.overflow = 'hidden';
    } else {
      document.body.style.overflow = '';
    }
  }, [isFullscreen]);

  const handleDragEnd = (e: L.LeafletEvent, type: 'group' | 'sub' | 'child', id: string) => {
    const marker = e.target as L.Marker;
    const { lat, lng } = marker.getLatLng();

    // 1. Calculate delta if part of a multi-selection
    const markerKey = `${type}-${id}`;
    let dLat = 0;
    let dLon = 0;

    if (allowMultiMove && selectedIds.has(markerKey)) {
      // Find original position to calc delta
      let oldLat = 0;
      let oldLon = 0;
      if (type === 'group') {
        oldLat = parseFloat(String(groupCoordinates[id]?.lat || 0));
        oldLon = parseFloat(String(groupCoordinates[id]?.lon || 0));
      } else if (type === 'sub') {
        oldLat = parseFloat(String(processes[parseInt(id)]?.lat || 0));
        oldLon = parseFloat(String(processes[parseInt(id)]?.lon || 0));
      } else if (type === 'child') {
        const [si, ci] = id.split('-').map(Number);
        oldLat = parseFloat(String(processes[si]?.children?.[ci]?.lat || 0));
        oldLon = parseFloat(String(processes[si]?.children?.[ci]?.lon || 0));
      }
      dLat = lat - oldLat;
      dLon = lng - oldLon;
    }

    // 2. Prepare updates
    let nextGroupCoords = { ...groupCoordinates };
    let nextProcesses = [...processes];
    let changedGroup = false;
    let changedProc = false;

    // Apply to selected set or just this marker
    const targets = (allowMultiMove && selectedIds.has(markerKey)) ? Array.from(selectedIds) : [markerKey];

    targets.forEach(key => {
      const [tType, tId] = key.split('-') as ['group' | 'sub' | 'child', string];
      let tLat = lat;
      let tLon = lng;

      if (key !== markerKey) {
        // Apply delta to others
        if (tType === 'group') {
          tLat = parseFloat(String(groupCoordinates[tId]?.lat || 0)) + dLat;
          tLon = parseFloat(String(groupCoordinates[tId]?.lon || 0)) + dLon;
        } else if (tType === 'sub') {
          tLat = parseFloat(String(processes[parseInt(tId)]?.lat || 0)) + dLat;
          tLon = parseFloat(String(processes[parseInt(tId)]?.lon || 0)) + dLon;
        } else if (tType === 'child') {
          const [si, ci] = tId.split('-').map(Number);
          tLat = parseFloat(String(processes[si]?.children?.[ci]?.lat || 0)) + dLat;
          tLon = parseFloat(String(processes[si]?.children?.[ci]?.lon || 0)) + dLon;
        }
      }

      if (tType === 'group') {
        nextGroupCoords[tId] = { ...(nextGroupCoords[tId] || {}), lat: tLat.toString(), lon: tLon.toString() };
        changedGroup = true;
      } else if (tType === 'sub') {
        const sIdx = parseInt(tId);
        nextProcesses[sIdx] = { ...nextProcesses[sIdx], lat: tLat.toString(), lon: tLon.toString() };
        changedProc = true;
      } else if (tType === 'child') {
        const [si, ci] = tId.split('-').map(Number);
        nextProcesses[si] = { ...nextProcesses[si] };
        nextProcesses[si].children = [...(nextProcesses[si].children || [])];
        nextProcesses[si].children[ci] = { ...nextProcesses[si].children[ci], lat: tLat.toString(), lon: tLon.toString() };
        changedProc = true;
      }
    });

    if (changedGroup && onGroupCoordinatesChange) onGroupCoordinatesChange(nextGroupCoords);
    if (changedProc && onProcessesChange) onProcessesChange(nextProcesses);
  };

  const tileUrl = TILE_URLS[baseTile] || TILE_URLS.OpenStreetMap;
  const isCanvasActive = useMemo(
    () => Object.values(childMapExpanded).some(Boolean) || Object.values(subprocessMapExpanded).some(Boolean),
    [childMapExpanded, subprocessMapExpanded]
  );

  return (
    <div
      className={`map-viewer-container ${className || ''} ${isFullscreen ? 'fullscreen' : ''} ${selectionActive ? 'pa-selection-active' : ''}`}
      style={!isFullscreen ? { height: height || '850px', flex: 'none', position: 'relative' } : { position: 'fixed', inset: 0, zIndex: 9999 }}
    >
      <div className="map-viewer-controls" style={{ position: 'absolute', top: 10, right: 10, zIndex: 1000, display: 'flex', gap: 8 }}>
        <button
          className="pa-btn"
          onClick={() => setIsFullscreen(!isFullscreen)}
          title={isFullscreen ? "Exit Fullscreen" : "Fullscreen"}
          style={{ padding: '6px 10px' }}
        >
          {isFullscreen ? '✕' : '⛶'}
        </button>
      </div>


      <MapContainer
        center={center}
        zoom={zoom}
        style={{ height: '100%', width: '100%' }}
        zoomControl={!locked}
        dragging={!locked}
        scrollWheelZoom={!locked}
        doubleClickZoom={!locked}
        boxZoom={!locked}
        keyboard={!locked}
        zoomSnap={0.25}
        zoomDelta={0.25}
      >
        <TileLayer url={tileUrl} />
        <MapController center={center} zoom={zoom} locked={locked} />
        <MapClickHandler onClick={handleMapClick} />
        <MapFullscreenResizer active={isFullscreen} />
        <MapMountFitter center={center} zoom={zoom} />

        {/* 1. Base Site Bubbles — Hidden by canvas if active */}
        <Pane name="streamBubblesPane" style={{ zIndex: 450 }}>
          <StreamCirclesOverlay
            processes={processes}
            groups={groups}
            groupNames={groupNames}
            groupCoordinates={groupCoordinates}
            selectedStreams={selectedStreams}
            onSelectionToggle={onSelectionToggle}
            onElementDoubleClick={onElementDoubleClick}
            locked={locked}
            onDragEnd={handleDragEnd}
          />
        </Pane>

        {/* 2. Focused Workshop Background — Above site markers, below expanded content */}
        <Pane name="canvasPane" style={{ zIndex: 650 }}>
          <SubprocessCanvasOverlay active={isCanvasActive} />
        </Pane>

        {/* 3. Main Site Markers — On top of canvas */}
        <Pane name="activeMarkerPane" style={{ zIndex: 600 }}>
          {groups.map((subIdxs, gIdx) => {
            const gCoords = groupCoordinates[gIdx];
            const showSubs = subprocessMapExpanded[gIdx];

            if (!showSubs) {
              const lat = parseFloat(String(gCoords?.lat ?? ''));
              const lon = parseFloat(String(gCoords?.lon ?? ''));
              if (isNaN(lat) || isNaN(lon)) return null;

              const gScale = gCoords?.box_scale ? parseFloat(String(gCoords.box_scale)) : 1.5;
              const isAnySelected = selectedStreams === undefined || subIdxs.some(si => {
                const p = processes[si];
                // If it has no streams, count as "selected" so it's opaque during placement
                if ((p.streams || []).length === 0) return true;
                return (p.streams || []).some((_, sidx) => selectedStreams?.[`stream_${si}_${sidx}`] !== false);
              });

              return (
                <Marker
                  key={`group-${gIdx}`}
                  position={[lat, lon]}
                  draggable={true}
                  pane="activeMarkerPane"
                  eventHandlers={{
                    click: () => {
                      if (onProcessesSelect) onProcessesSelect(subIdxs, !isAnySelected);
                    },
                    dragend: (e) => handleDragEnd(e, 'group', String(gIdx))
                  }}
                  icon={createDivIcon(
                    '',
                    groupNames[gIdx] || `Process ${gIdx + 1}`,
                    '',
                    gScale,
                    isAnySelected,
                    allowMultiMove && selectedIds.has(`group-${gIdx}`),
                    'group',
                    String(gIdx)
                  )}
                />
              );
            }
            return null;
          })}

          <ConnectionLines
            processes={processes}
            groups={groups}
            groupNames={groupNames}
            groupCoordinates={groupCoordinates}
            subprocessMapExpanded={subprocessMapExpanded}
            childMapExpanded={childMapExpanded}
          />
        </Pane>

        {/* 4. Hierarchical Expanded Content — Above site markers */}
        <Pane name="expandedContentPane" style={{ zIndex: 700 }}>
          {/* Subprocesses (if group expanded) */}
          {groups.map((subIdxs, gIdx) => {
            if (!subprocessMapExpanded[gIdx]) return null;
            return subIdxs.map((si) => {
              const p = processes[si];
              if (!p) return null;
              const lat = parseFloat(String(p.lat));
              const lon = parseFloat(String(p.lon));
              if (isNaN(lat) || isNaN(lon)) return null;

              const isSubSelected = selectedStreams === undefined || (p.streams || []).length === 0 || (p.streams || []).some((_, sidx) => selectedStreams?.[`stream_${si}_${sidx}`] !== false);

              return (
                <Marker
                  key={`sub-${si}`}
                  position={[lat, lon]}
                  draggable={true}
                  pane="expandedContentPane"
                  eventHandlers={{
                    click: () => {
                      if (onProcessesSelect) onProcessesSelect([si], !isSubSelected);
                    },
                    dragend: (e) => handleDragEnd(e, 'sub', String(si))
                  }}
                  icon={createDivIcon(
                    '',
                    p.name,
                    '',
                    p.box_scale ? parseFloat(String(p.box_scale)) : 1.2,
                    isSubSelected,
                    allowMultiMove && selectedIds.has(`sub-${si}`),
                    'sub',
                    String(si)
                  )}
                />
              );
            });
          })}

          {/* Children (if subprocess expanded) */}
          {processes.map((p, si) => {
            if (!childMapExpanded[si]) return null;
            return (p.children || []).map((child, ci) => {
              const lat = parseFloat(String(child.lat));
              const lon = parseFloat(String(child.lon));
              if (isNaN(lat) || isNaN(lon)) return null;

              const isChildSelected = selectedStreams === undefined || (child.streams || []).length === 0 || (child.streams || []).some((_, sidx) => selectedStreams?.[`stream_${si}_${ci}_${sidx}`] !== false);

              return (
                <Marker
                  key={`child-${si}-${ci}`}
                  position={[lat, lon]}
                  draggable={true}
                  pane="expandedContentPane"
                  eventHandlers={{
                    dragend: (e) => handleDragEnd(e, 'child', `${si}-${ci}`)
                  }}
                  icon={createDivIcon(
                    '',
                    child.name,
                    '',
                    child.box_scale ? parseFloat(String(child.box_scale)) : 0.9,
                    isChildSelected,
                    allowMultiMove && selectedIds.has(`child-${si}-${ci}`),
                    'child',
                    `${si}-${ci}`
                  )}
                />
              );
            });
          })}
        </Pane>

        {/* 5. Hierarchical Expanded Bubbles — Top-most interaction */}
        <Pane name="expandedBubblesPane" style={{ zIndex: 750 }}>
          <StreamCirclesOverlay
            processes={processes}
            groups={[]}
            groupNames={[]}
            groupCoordinates={{}}
            selectedStreams={selectedStreams}
            onSelectionToggle={onSelectionToggle}
            onElementDoubleClick={onElementDoubleClick}
            locked={locked}
            onDragEnd={handleDragEnd}
            renderingMode="children"
            childMapExpanded={childMapExpanded}
          />
        </Pane>

        <BoxSelector
          active={locked} // Always active when map is locked
          onSelect={(bounds) => {
            lastSelectionTime.current = Date.now();
            const idxsInBox: number[] = [];
            const newSelected = new Set<string>();

            // 1. Check groups
            groups.forEach((subIdxs, gIdx) => {
              const gc = groupCoordinates[gIdx];
              if (gc && gc.lat && gc.lon) {
                const lat = parseFloat(String(gc.lat));
                const lon = parseFloat(String(gc.lon));
                if (bounds.contains([lat, lon])) {
                  idxsInBox.push(...subIdxs);
                  if (allowMultiMove) newSelected.add(`group-${gIdx}`);
                }
              }
            });

            // 2. Check independent processes
            processes.forEach((p, pi) => {
              if (p.lat && p.lon) {
                const lat = parseFloat(String(p.lat));
                const lon = parseFloat(String(p.lon));
                if (bounds.contains([lat, lon])) {
                  idxsInBox.push(pi);
                  if (allowMultiMove) newSelected.add(`sub-${pi}`);
                }
              }
            });

            if (allowMultiMove) setSelectedIds(newSelected);

            // 3. Smart Toggle for Analysis Streams
            if (onProcessesSelect && idxsInBox.length > 0) {
              const uniqueIdxs = Array.from(new Set(idxsInBox));

              const allInBoxSelected = uniqueIdxs.every(pi => {
                const p = processes[pi];
                return (p.streams || []).every((_: any, si: number) => selectedStreams?.[`stream_${pi}_${si}`] !== false);
              });

              onProcessesSelect(uniqueIdxs, !allInBoxSelected);
            }
          }}
        />



        {onMoveEnd && <MapMoveHandler onMoveEnd={onMoveEnd} locked={locked} />}
        <MapMountFitter center={center} zoom={zoom} />
        <MapFullscreenResizer active={isFullscreen} />
      </MapContainer>
    </div>
  );
}
