// ──────────────────────────────────────────────────────────────
// GeoMap — Leaflet-based interactive map for case-study dashboards.
// Uses Esri World Light Gray Base tiles (no API key required).
// Drill-down: zoom in to move from regions → cities → street scans.
// ──────────────────────────────────────────────────────────────
const { useState: useGM, useEffect: useGMEffect, useRef: useGMRef, useMemo: useGMMemo } = React;

function gmLoadLeaflet() {
  if (window.__leafletReady) return window.__leafletReady;
  window.__leafletReady = new Promise((resolve, reject) => {
    const link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
    document.head.appendChild(link);
    // Inject tooltip styles
    const style = document.createElement('style');
    style.textContent = `.gm-tip{background:#1C2250;border:none;color:#fff;font-family:Montserrat,sans-serif;font-size:11px;font-weight:700;padding:4px 10px;border-radius:6px;box-shadow:0 2px 8px rgba(28,34,80,0.25);white-space:nowrap}.gm-tip::before{border-top-color:#1C2250!important}.leaflet-tooltip-bottom.gm-tip::before{border-bottom-color:#1C2250!important;border-top-color:transparent!important}`;
    document.head.appendChild(style);
    const s = document.createElement('script');
    s.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
    s.onload = () => resolve(window.L);
    s.onerror = () => reject(new Error('Failed to load Leaflet'));
    document.head.appendChild(s);
  });
  return window.__leafletReady;
}



const REGION_STARTS = {
  US:              { center: [38, -97],       zoom: 4 },
  UK:              { center: [54.5, -3],      zoom: 6 },
  Global:          { center: [20, 0],         zoom: 2 },
  Canada:          { center: [56, -96],       zoom: 4 },
  'North America': { center: [44, -100],      zoom: 3 },
  Toronto:         { center: [43.65, -79.38], zoom: 10 },
};

// Seeded deterministic random (same seed logic as old gmBuild)
function seeded(str) {
  let h = 2166136261;
  for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
  return () => {
    h += 0x6D2B79F5; let t = h;
    t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function splitValue(total, n, rng) {
  if (n <= 0) return [];
  const w = Array.from({ length: n }, () => 0.55 + rng());
  const s = w.reduce((a, b) => a + b, 0);
  return w.map(x => Math.max(1, Math.round((total || n) * x / s)));
}

function buildTiers(data, scanDensity) {
  const DICT = window.TS_GEO_CHILDREN || {};
  const CITY_REGION = window.TS_CITY_REGION || {};

  const src = data
    .filter(d => typeof d.lon === 'number' && typeof d.lat === 'number')
    .map(d => ({ label: d.label, lon: d.lon, lat: d.lat, value: d.value || 0 }));

  const cityKeyed = src.filter(d => CITY_REGION[d.label]).length > src.length / 2;

  let tRegions, tCities;

  if (cityKeyed) {
    tCities = src.map(d => ({ ...d }));
    const groups = {};
    tCities.forEach(c => { const r = CITY_REGION[c.label] || 'Other'; (groups[r] = groups[r] || []).push(c); });
    tRegions = Object.keys(groups).map(label => {
      const cs = groups[label];
      return { label, lon: cs.reduce((s,c)=>s+c.lon,0)/cs.length, lat: cs.reduce((s,c)=>s+c.lat,0)/cs.length, value: cs.reduce((s,c)=>s+(c.value||0),0) };
    });
  } else {
    tRegions = src.map(d => ({ ...d }));
    tCities = [];
    tRegions.forEach(r => {
      const rng = seeded(r.label+'|'+r.lon.toFixed(3)+'|'+r.lat.toFixed(3));
      const defs = (DICT[r.label]||[]).map(([label,lon,lat]) => ({label,lon,lat}));
      const cityDefs = defs.length ? defs : Array.from({length:3+Math.floor(rng()*2)},(_,i) => ({
        label: r.label+' '+['Metro','North','South','East','West'][i%5],
        lon: r.lon+(rng()-0.5)*1.6, lat: r.lat+(rng()-0.5)*1.2
      }));
      const vals = splitValue(r.value, cityDefs.length, rng);
      cityDefs.forEach((c,i) => tCities.push({label:c.label,lon:c.lon,lat:c.lat,value:vals[i],parent:r.label}));
    });
  }

  // Street-level scans: clustered tightly around each city centre (on land),
  // count proportional to city value so dense urban areas get more dots.
  // Spread of ±0.25 lon / ±0.18 lat ≈ ±25 km — stays on land for virtually
  // all known cities. Gaussian-like by summing two uniform samples (CLT).
  const tScans = [];
  const totalValue = tCities.reduce((s,c) => s + (c.value||1), 0);
  const totalScans = Math.round(16 * tCities.length * scanDensity);
  tCities.forEach(c => {
    const rng = seeded('scan|'+c.label+'|'+c.lon.toFixed(3)+'|'+c.lat.toFixed(3));
    // Weight scan count by this city's share of total value
    const share = (c.value||1) / Math.max(1, totalValue);
    const target = Math.max(2, Math.round(totalScans * share));
    const vals = splitValue(c.value, target, rng);
    for (let i = 0; i < target; i++) {
      // Place scans at known towns near this city (from TOWN_DB), fall back to parent region
      const pool = (window.GM_TOWN_DB && (window.GM_TOWN_DB[c.label] || window.GM_TOWN_DB[c.parent])) || [];
      let lon, lat;
      if (pool.length) {
        // Random selection (not cycling) gives natural spread across all towns
        const idx = Math.floor(rng() * pool.length);
        const t = pool[idx];
        lon = t[0] + ((rng()-0.5)+(rng()-0.5)) * 0.18;
        lat = t[1] + ((rng()-0.5)+(rng()-0.5)) * 0.13;
      } else {
        lon = c.lon + ((rng()-0.5)+(rng()-0.5)) * 0.3;
        lat = c.lat + ((rng()-0.5)+(rng()-0.5)) * 0.2;
      }
      tScans.push({ label:'Scan', lon, lat, value:vals[i]||1, parent:c.label, activated:rng()<0.82 });
    }
  });

  return [tRegions, tCities, tScans];
}

const LEVEL_NAMES = ['Regions', 'Cities', 'Street level'];

function GeoMap({ data, type = 'other', region = 'US', format, scanDensity = 1 }) {
  const fmt = format || ((v) => typeof v === 'number' ? v.toLocaleString() : String(v));
  const containerRef = useGMRef(null);
  const mapRef = useGMRef(null);
  const layerGroupRef = useGMRef(null);
  const tiersRef = useGMRef(null);
  const [uiLevel, setUiLevel] = useGM(0);
  const [onlyNotActivated, setOnlyNotActivated] = useGM(false);
  const onlyNotActivatedRef = useGMRef(onlyNotActivated);
  onlyNotActivatedRef.current = onlyNotActivated;
  const [isTouch, setIsTouch] = useGM(false);
  useGMEffect(() => {
    try { setIsTouch(window.matchMedia('(pointer: coarse)').matches); } catch(_) {}
  }, []);
  const actionWord = isTouch ? 'Tap' : 'Click';

  const palette = (window.TSChartPalette && window.TSChartPalette(type)) || ['#2F3886','#5D67BA','#8B92CD'];
  const color = palette[0];
  const isAccount = type === 'accountability';
  const GM_GREEN = '#1F8A5B', GM_RED = '#DB4A3F';
  const streetTerm = isAccount ? 'Activations' : (type === 'experience' ? 'Engagements' : 'Scans');

  // Build data tiers synchronously
  tiersRef.current = useGMMemo(() => buildTiers(data, scanDensity), [data, scanDensity]);

  const drawMarkers = (lv) => {
    const map = mapRef.current;
    const layerGroup = layerGroupRef.current;
    if (!map || !layerGroup || !tiersRef.current || !window.L) return;
    const L = window.L;
    layerGroup.clearLayers();
    let tierArr = tiersRef.current[lv] || [];
    if (isAccount && lv === 2 && onlyNotActivatedRef.current) {
      tierArr = tierArr.filter(m => !m.activated);
    }
    const maxVal = tierArr.reduce((m,d) => Math.max(m, d.value||0), 1);

    tierArr.forEach(m => {
      const norm = (m.value||0) / maxVal;
      const r = lv === 2 ? 3 + norm*5 : 6 + norm*11;
      const mColor = (isAccount && lv === 2) ? (m.activated ? GM_GREEN : GM_RED) : color;
      const label = lv === 2 ? (isAccount ? (m.activated ? 'Activated' : 'Not Activated') : streetTerm) : m.label;
      const tipContent = `<span>${label}</span>${lv < 2 ? ' — '+fmt(m.value) : ''}`;

      L.circleMarker([m.lat, m.lon], {
        radius: r+8, fillColor: mColor, color:'none', weight:0, fillOpacity:0.13, interactive:false,
      }).addTo(layerGroup);

      const marker = L.circleMarker([m.lat, m.lon], {
        radius: r, fillColor: mColor, color:'#fff', weight:1.5, fillOpacity:0.92,
      });
      marker.bindTooltip(tipContent, { className:'gm-tip', direction:'top', offset:[0,-r-2] });
      // Permanent name label for regions and cities
      if (lv < 2) {
        marker.bindTooltip(m.label, {
          className:'gm-tip', permanent:true, direction:'bottom',
          offset:[0, r+4],
        });
      }
      marker.addTo(layerGroup);
    });
  };

  // Initialise Leaflet map
  useGMEffect(() => {
    let map = null;
    gmLoadLeaflet().then(L => {
      if (!containerRef.current) return;
      const cfg = REGION_STARTS[region] || REGION_STARTS.US;
      map = L.map(containerRef.current, {
        center: cfg.center, zoom: cfg.zoom,
        zoomControl: false, attributionControl: false,
      });
      L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}', {
        maxZoom: 19, maxNativeZoom: 16,
      }).addTo(map);
      // tiny attribution
      const attr = document.createElement('div');
      attr.style.cssText = 'position:absolute;bottom:4px;left:50%;transform:translateX(-50%);font-family:Montserrat;font-size:9px;color:#8B92CD;pointer-events:none;z-index:1000;white-space:nowrap';
      attr.innerHTML = 'Tiles © <a href="https://www.esri.com" style="color:#8B92CD">Esri</a> — Esri, HERE, Garmin, OpenStreetMap contributors';
      containerRef.current.appendChild(attr);

      const layerGroup = L.layerGroup().addTo(map);
      layerGroupRef.current = layerGroup;
      mapRef.current = map;
      // Rebuild tiers NOW — land features guaranteed loaded at this point
      const getLevel = (z) => 2;
      const onZoom = () => {
        const lv = getLevel(map.getZoom());
        setUiLevel(lv);
        drawMarkers(lv);
      };
      map.on('zoomend', onZoom);
      onZoom();
    }).catch(e => console.warn('GeoMap: Leaflet load failed', e));

    return () => { if (map) { try { map.remove(); } catch(_){} mapRef.current = null; layerGroupRef.current = null; } };
  }, [region]);

  // Redraw markers when data changes (after initial mount)
  useGMEffect(() => {
    if (!mapRef.current) return;
    const lv = mapRef.current.getZoom ? (mapRef.current.getZoom() >= 11 ? 2 : mapRef.current.getZoom() >= 7 ? 1 : 0) : 0;
    setUiLevel(lv);
    drawMarkers(lv);
  }, [data]);

  // Redraw when the "only not activated" toggle changes
  useGMEffect(() => {
    if (!mapRef.current) return;
    drawMarkers(uiLevel);
  }, [onlyNotActivated]);

  const btn = {
    width:30, height:30, borderRadius:8, border:'1px solid #E1E4F2', background:'#fff',
    color:'#1C2250', cursor:'pointer', fontSize:17, fontWeight:700, lineHeight:1,
    display:'grid', placeItems:'center', boxShadow:'0 2px 6px rgba(28,34,80,0.12)',
    fontFamily:'Montserrat', zIndex:1000,
  };

  const zoom = (delta) => { if (mapRef.current) delta > 0 ? mapRef.current.zoomIn() : mapRef.current.zoomOut(); };
  const reset = () => {
    if (!mapRef.current) return;
    const cfg = REGION_STARTS[region] || REGION_STARTS.US;
    mapRef.current.setView(cfg.center, cfg.zoom);
  };

  return (
    <div style={{ position:'absolute', inset:0, borderRadius:12, overflow:'hidden' }}>
      <div ref={containerRef} style={{ width:'100%', height:'100%' }} />

      {/* Zoom controls */}
      <div style={{ position:'absolute', right:10, bottom:10, display:'flex', flexDirection:'column', gap:6, zIndex:1000 }}>
        <button style={btn} title="Zoom in"  onClick={() => zoom(1)}>+</button>
        <button style={btn} title="Zoom out" onClick={() => zoom(-1)}>−</button>
        <button style={{...btn, fontSize:12}} title="Reset" onClick={reset}>⤺</button>
      </div>

      {/* Toggle: show only Not Activated bubbles (accountability dashboards only) */}
      {isAccount && (
        <button
          onClick={() => setOnlyNotActivated(v => !v)}
          title={onlyNotActivated ? actionWord + ' to show All Assets' : actionWord + ' to show Only Not Actived Assets'}
          style={{
            position:'absolute', left:10, bottom:10, zIndex:1000,
            display:'flex', alignItems:'center', gap:7,
            height:30, padding:'0 12px', borderRadius:999,
            border: '1px solid ' + (onlyNotActivated ? '#E1E4F2' : GM_RED),
            background: onlyNotActivated ? '#fff' : GM_RED,
            color: onlyNotActivated ? '#1C2250' : '#fff',
            cursor:'pointer', fontSize:11.5, fontWeight:700,
            fontFamily:'Montserrat', boxShadow:'0 2px 6px rgba(28,34,80,0.12)',
            transition:'all 160ms',
          }}>
          <span style={{ width:8, height:8, borderRadius:999, background: onlyNotActivated ? GM_GREEN : '#fff', flexShrink:0 }} />
          {onlyNotActivated ? actionWord + ' to show All Assets' : actionWord + ' to show Only Not Actived Assets'}
        </button>
      )}


    </div>
  );
}

// Error boundary — catches render errors, shows neutral bg, auto-recovers
class GeoMapBoundary extends React.Component {
  constructor(props) { super(props); this.state = { failed: false, failKey: 0 }; }
  static getDerivedStateFromError() { return { failed: true }; }
  componentDidCatch(err) { try { console.warn('GeoMap error, recovering:', err && err.message); } catch(_) {}
    setTimeout(() => { try { this.setState(s => ({ failed:false, failKey:s.failKey+1 })); } catch(_) {} }, 120);
  }
  componentDidUpdate(prev) {
    if (this.state.failed && (prev.region !== this.props.region || prev.data !== this.props.data))
      this.setState({ failed:false, failKey:this.state.failKey+1 });
  }
  render() {
    if (this.state.failed) return <div style={{ position:'absolute', inset:0, background:'#EEF1FB', borderRadius:12 }} />;
    return <GeoMap key={this.state.failKey} {...this.props} />;
  }
}

window.TSGeoMap = (props) => <GeoMapBoundary {...props} />;
