/* ===== Basemap — illustrated, hand-drawn "Novale Design Base Plan Style" =====
   In Mapbox mode, the SVG is mostly suppressed and real GeoJSON layers from
   King County GIS + national sources (NLCD canopy, USDA soils, USDA hardiness)
   trace the actual property. SiteAnalysis owns the fetch lifecycle and passes
   the resolved data in via the `siteData` prop; BaseMap only renders.
*/

const BaseMap = ({
  layers = {
    boundary: true,
    structures: true,
    trees: true,
    sun: false,
    soil: false,
    utilities: false,
    wetland: false,
    zones: false,
    contours: false,
    flood: false,
    critical: false,
    labels: true,
  },
  highlight = null,
  interactive = false,
  designArea = false,
  mode = 'basemap',
  plants = false,
  width = 900, height = 560,
  site = null,
  siteData = null,
}) => {
  const cx = 450, cy = 280;

  const mapboxToken = (window.NOVALE_CONFIG && window.NOVALE_CONFIG.MAPBOX_TOKEN) || '';
  const useMapbox = Boolean(
    mapboxToken && site && Number.isFinite(site.lat) && Number.isFinite(site.lng) &&
    mode !== 'cad' && mode !== 'render' && window.mapboxgl,
  );

  // Internal fetch fallback for parcel + building when SiteAnalysis isn't
  // managing data for this caller (e.g. DesignReveal preview). When siteData is
  // provided we trust it and skip these fetches.
  const [internalParcel, setInternalParcel] = React.useState(null);
  const [internalBuilding, setInternalBuilding] = React.useState(null);

  React.useEffect(() => {
    if (!useMapbox || siteData || !window.fetchParcel || !window.fetchBuildings) return;
    let cancelled = false;
    setInternalParcel(null);
    setInternalBuilding(null);
    (async () => {
      const [p, bs] = await Promise.all([
        window.fetchParcel(site.lat, site.lng),
        window.fetchBuildings(site.lat, site.lng),
      ]);
      if (cancelled) return;
      setInternalParcel(p);
      setInternalBuilding(window.pickPrimaryBuilding?.(bs, p, site.lat, site.lng) || null);
    })();
    return () => { cancelled = true; };
  }, [useMapbox, siteData, site?.lat, site?.lng]);

  const parcel = siteData?.parcel ?? internalParcel;
  const building = siteData?.building ?? internalBuilding;
  const zoning = siteData?.zoning ?? null;
  const setbacks = siteData?.setbacks ?? null;
  const contours = siteData?.contours ?? null;
  const wetlands = siteData?.wetlands ?? null;
  const streams = siteData?.streams ?? null;
  const criticalAreas = siteData?.criticalAreas ?? null;
  const flood = siteData?.flood ?? null;
  const soils = siteData?.soils ?? null;
  const treeCanopyBbox = siteData?.treeCanopyBbox ?? null;
  const canopyUrl = siteData?.canopyUrl ?? null;

  // Address-derived labels for the SVG overlay (always available).
  const streetLabel = ((site?.streetName || '378TH AVE SE')
    .replace(/^\d+\s*/, '')
    .toUpperCase());
  const lotLabel = site?.locality ? `LOT · ${site.locality.toUpperCase()}` : 'LOT · 0.42 ac';

  const bg = mode === 'cad' ? '#FFFFFF' : '#FAF5EC';
  const ink = mode === 'cad' ? '#1C1917' : '#3D4421';
  const houseFill = mode === 'cad' ? '#FFFFFF' : '#E8D9BE';
  const houseStroke = mode === 'cad' ? '#1C1917' : '#7C5A3A';
  const drivewayFill = mode === 'cad' ? '#F4F2EC' : '#E8E5DA';
  const lawnFill = mode === 'render' ? '#7A9248' : (mode === 'cad' ? '#FFFFFF' : '#D1D7AB');
  const lawnStroke = mode === 'cad' ? '#1C1917' : 'transparent';

  const svg = (
    <svg
      viewBox={`0 0 ${width} ${height}`}
      className={`basemap${useMapbox ? ' basemap--overlay' : ''}`}
      preserveAspectRatio="xMidYMid slice"
      style={{ width: '100%', height: '100%', display: 'block' }}
    >
      <defs>
        <pattern id="bm-grid" width="24" height="24" patternUnits="userSpaceOnUse">
          <path d="M24 0 L0 0 0 24" fill="none" stroke={mode === 'cad' ? '#D6D2C2' : '#E8E5DA'} strokeWidth="0.5" />
        </pattern>
        <radialGradient id="bm-sun" cx="75%" cy="18%" r="80%">
          <stop offset="0%" stopColor="#FDE68A" stopOpacity="0.85" />
          <stop offset="40%" stopColor="#FDE68A" stopOpacity="0.35" />
          <stop offset="100%" stopColor="#3D4421" stopOpacity="0.18" />
        </radialGradient>
        <pattern id="bm-soil-clay" width="8" height="8" patternUnits="userSpaceOnUse" patternTransform="rotate(35)">
          <rect width="8" height="8" fill="#D97757" fillOpacity="0.12" />
          <circle cx="2" cy="2" r="0.8" fill="#C25E3E" fillOpacity="0.45" />
        </pattern>
        <pattern id="bm-soil-loam" width="10" height="10" patternUnits="userSpaceOnUse">
          <rect width="10" height="10" fill="#B0BB7E" fillOpacity="0.12" />
          <circle cx="5" cy="5" r="0.6" fill="#3D4421" fillOpacity="0.35" />
        </pattern>
        <filter id="bm-watercolor" x="-5%" y="-5%" width="110%" height="110%">
          <feTurbulence baseFrequency="0.9" numOctaves="2" seed="3" />
          <feDisplacementMap in="SourceGraphic" scale="1.2" />
        </filter>
      </defs>

      {!useMapbox && (
        <>
          <rect width={width} height={height} fill={bg} />
          <rect width={width} height={height} fill="url(#bm-grid)" opacity={mode === 'cad' ? 1 : 0.5} />
        </>
      )}

      {mode === 'render' && (
        <g>
          <rect width={width} height={height} fill="#5A7A2E" />
          <rect width={width} height={height} fill="#3D4421" opacity="0.35" />
          <rect width={width} height={height} fill="url(#bm-sun)" opacity="0.3" />
        </g>
      )}

      {!useMapbox && mode !== 'render' && (
        <g opacity="0.35">
          <rect x="0" y="0" width="120" height={height} fill={mode === 'cad' ? '#F4F2EC' : '#F4EFE2'} />
          <rect x={width - 120} y="0" width="120" height={height} fill={mode === 'cad' ? '#F4F2EC' : '#F4EFE2'} />
        </g>
      )}

      {!useMapbox && (
        <g opacity={mode === 'render' ? 0.7 : 1}>
          <rect x="0" y="0" width={width} height="44" fill={mode === 'cad' ? '#FFFFFF' : '#D6D2C2'} stroke={mode === 'cad' ? '#1C1917' : 'none'} strokeWidth="0.8" />
          <line x1="0" y1="22" x2={width} y2="22" stroke={mode === 'cad' ? '#1C1917' : '#A8A294'} strokeWidth="0.8" strokeDasharray="12 8" />
          {layers.labels && mode !== 'render' && (
            <text x="24" y="14" fontFamily="Inter, sans-serif" fontSize="9" fontWeight="600" fill={ink} letterSpacing="0.12em">
              {streetLabel}
            </text>
          )}
        </g>
      )}

      {!useMapbox && (
        <rect x="130" y="60" width="640" height="460" fill={lawnFill} stroke={lawnStroke} strokeWidth="0.8" />
      )}

      {/* Lot boundary — SVG only when no real parcel is in play */}
      {layers.boundary && !useMapbox && (
        <g className={highlight === 'boundary' ? 'bm-pulse' : ''}>
          <rect x="130" y="60" width="640" height="460"
            fill="none"
            stroke={mode === 'cad' ? '#1C1917' : '#C25E3E'}
            strokeWidth={mode === 'cad' ? 1.5 : 2}
            strokeDasharray={mode === 'cad' ? '0' : '10 6'}
          />
          {[[130,60],[770,60],[770,520],[130,520]].map((p,i) => (
            <circle key={i} cx={p[0]} cy={p[1]} r="4" fill={mode === 'cad' ? '#fff' : '#FAF9F6'} stroke={mode === 'cad' ? '#1C1917' : '#C25E3E'} strokeWidth="1.5" />
          ))}
          {layers.labels && mode !== 'render' && (
            <text x="140" y="78" fontFamily="Geist Mono, monospace" fontSize="9" fill={mode === 'cad' ? '#1C1917' : '#C25E3E'}>
              {lotLabel}
            </text>
          )}
        </g>
      )}

      {/* Sun / shade — kept as decorative SVG (no public sun-aspect dataset) */}
      {layers.sun && !useMapbox && (
        <g className={highlight === 'sun' ? 'bm-pulse' : ''}>
          <rect x="130" y="60" width="640" height="460" fill="url(#bm-sun)" />
          <ellipse cx="230" cy="380" rx="90" ry="70" fill="#3D4421" opacity="0.22" filter={mode === 'watercolor' ? 'url(#bm-watercolor)' : undefined} />
          <ellipse cx="720" cy="160" rx="55" ry="45" fill="#3D4421" opacity="0.22" />
          {layers.labels && (
            <>
              <text x="560" y="180" fontFamily="Inter" fontSize="10" fontWeight="600" fill="#92400E" letterSpacing="0.1em">FULL SUN · 6–8 HRS</text>
              <text x="200" y="330" fontFamily="Inter" fontSize="10" fontWeight="600" fill={ink} letterSpacing="0.1em">PART SHADE</text>
            </>
          )}
        </g>
      )}

      {/* Utilities — decorative only; no public source for buried lines */}
      {layers.utilities && !useMapbox && (
        <g className={highlight === 'utilities' ? 'bm-pulse' : ''}>
          <line x1="130" y1="150" x2="770" y2="150" stroke="#B45309" strokeWidth="1.5" strokeDasharray="4 3" />
          <line x1="340" y1="60" x2="340" y2="520" stroke="#B45309" strokeWidth="1.5" strokeDasharray="4 3" />
          {layers.labels && (
            <text x="580" y="144" fontFamily="Geist Mono" fontSize="9" fill="#B45309">GAS · 3ft depth</text>
          )}
        </g>
      )}

      {/* Hardscape — SVG only when no real building footprint is in play */}
      {layers.structures && !useMapbox && (
        <g>
          <path d="M 280 60 L 280 240 L 360 240 L 360 60 Z" fill={drivewayFill} stroke={mode === 'cad' ? '#1C1917' : '#A8A294'} strokeWidth={mode === 'cad' ? 1 : 0.8} />
          {mode !== 'render' && mode !== 'cad' && (
            <g opacity="0.5">
              {Array.from({length: 8}).map((_, i) => (
                <line key={i} x1="280" y1={70 + i*22} x2="360" y2={70 + i*22} stroke="#A8A294" strokeWidth="0.4" />
              ))}
            </g>
          )}
          <path d="M 270 240 L 520 240 L 520 370 L 420 370 L 420 410 L 270 410 Z"
            fill={houseFill} stroke={houseStroke} strokeWidth="1.8" />
          {mode === 'cad' && (
            <g opacity="0.4">
              {Array.from({length: 14}).map((_, i) => (
                <line key={i} x1={270 + i*18} y1="240" x2={270 + i*18 - 30} y2="410" stroke="#1C1917" strokeWidth="0.4" />
              ))}
            </g>
          )}
          {layers.labels && (
            <text x="362" y="320" fontFamily="Inter" fontSize="12" fontWeight="600" fill={ink} textAnchor="middle">HOUSE</text>
          )}
          <path d="M 360 410 L 400 410 L 400 450 L 360 450 Z" fill={drivewayFill} stroke={mode === 'cad' ? '#1C1917' : '#A8A294'} strokeWidth="0.8" />
          <path d="M 360 450 L 400 450 L 400 520 L 360 520 Z" fill={drivewayFill} stroke={mode === 'cad' ? '#1C1917' : '#A8A294'} strokeWidth="0.6" strokeDasharray={mode === 'cad' ? '0' : '3 3'} />
          <rect x="300" y="410" width="60" height="40" fill={drivewayFill} stroke={mode === 'cad' ? '#1C1917' : '#A8A294'} strokeWidth="0.8" />
        </g>
      )}

      {designArea && (
        <g>
          <rect x="140" y="420" width="620" height="92" fill="#D97757" fillOpacity="0.12" stroke="#D97757" strokeWidth="1.5" strokeDasharray="6 4" rx="4" />
          <text x="450" y="475" fontFamily="Fraunces, serif" fontSize="14" fontStyle="italic" fontWeight="500" fill="#9F4A30" textAnchor="middle">
            Design area · ~4,200 ft²
          </text>
        </g>
      )}

      {/* Decorative trees — only in pure SVG mode */}
      {layers.trees && !useMapbox && (
        <g className={highlight === 'trees' ? 'bm-pulse' : ''}>
          <ExistingTree x={200} y={380} r={32} kind="conifer" mode={mode} />
          <ExistingTree x={168} y={320} r={24} kind="conifer" mode={mode} />
          <ExistingTree x={230} y={440} r={22} kind="conifer" mode={mode} />
          <ExistingTree x={600} y={160} r={28} kind="maple" mode={mode} />
          <ExistingTree x={700} y={180} r={22} kind="maple" mode={mode} />
          <ExistingTree x={680} y={470} r={26} kind="oak" mode={mode} />
        </g>
      )}

      {plants && <PlantingPlan mode={mode} />}

      {mode !== 'render' && (
        <g transform={`translate(${width - 80}, ${height - 70})`}>
          <circle r="24" fill={mode === 'cad' ? '#fff' : 'rgba(250,249,246,0.85)'} stroke={ink} strokeWidth="1" />
          <path d="M 0 -16 L 4 0 L 0 3 L -4 0 Z" fill={ink} />
          <path d="M 0 16 L 4 0 L 0 -3 L -4 0 Z" fill="none" stroke={ink} strokeWidth="0.8" />
          <text y="-26" fontFamily="Geist Mono" fontSize="8" fill={ink} textAnchor="middle">N</text>
        </g>
      )}

      {mode !== 'render' && (
        <g transform={`translate(30, ${height - 30})`}>
          <line x1="0" y1="0" x2="80" y2="0" stroke={ink} strokeWidth="1.2" />
          <line x1="0" y1="-4" x2="0" y2="4" stroke={ink} strokeWidth="1.2" />
          <line x1="40" y1="-3" x2="40" y2="3" stroke={ink} strokeWidth="1" />
          <line x1="80" y1="-4" x2="80" y2="4" stroke={ink} strokeWidth="1.2" />
          <text x="0" y="-8" fontFamily="Geist Mono" fontSize="9" fill={ink}>0</text>
          <text x="80" y="-8" fontFamily="Geist Mono" fontSize="9" fill={ink} textAnchor="middle">40 ft</text>
        </g>
      )}

      {mode === 'cad' && (
        <g transform={`translate(${width - 220}, 60)`}>
          <rect width="200" height="80" fill="#fff" stroke="#1C1917" strokeWidth="0.8" />
          <line y1="22" x2="200" y2="22" stroke="#1C1917" strokeWidth="0.5" />
          <line y1="48" x2="200" y2="48" stroke="#1C1917" strokeWidth="0.5" />
          <text x="100" y="15" fontFamily="Fraunces, serif" fontSize="11" fontWeight="500" fill="#1C1917" textAnchor="middle">NOVALE · PLANTING PLAN</text>
          <text x="8" y="37" fontFamily="Geist Mono" fontSize="8" fill="#1C1917">
            {(site?.streetName ? `${site.streetName.toUpperCase()}` : 'LOT 8602')}
            {site?.locality ? ` · ${site.locality.toUpperCase()} ${site?.region || ''}` : ' · SNOQUALMIE WA'}
          </text>
          <text x="8" y="62" fontFamily="Geist Mono" fontSize="8" fill="#1C1917">SCALE 1/16" = 1'</text>
          <text x="120" y="62" fontFamily="Geist Mono" fontSize="8" fill="#1C1917">APR 2026 · L-01</text>
        </g>
      )}
    </svg>
  );

  if (!useMapbox) return svg;

  return (
    <div className="basemap-stack" style={{ width: '100%', height: '100%' }}>
      <MapboxBase
        lat={site.lat}
        lng={site.lng}
        token={mapboxToken}
        zoom={19}
        parcel={parcel}
        building={building}
        zoning={zoning}
        setbacks={setbacks}
        contours={contours}
        wetlands={wetlands}
        streams={streams}
        criticalAreas={criticalAreas}
        flood={flood}
        soils={soils}
        treeCanopyBbox={treeCanopyBbox}
        canopyUrl={canopyUrl}
        layers={layers}
        highlight={highlight}
      />
      <div className="basemap-stack__overlay">{svg}</div>
      <div className="basemap-stack__grain" aria-hidden="true" />
    </div>
  );
};

/* ==== Mapbox satellite backdrop with Novale-styled GIS overlay ==== */

// All Mapbox layer ids — kept as a single registry so we can iterate for
// visibility wiring and cleanup. `kind` distinguishes vector (data-driven) from
// raster (image-source-driven) layers.
const NOVALE_LAYERS = [
  // Hydro
  { id: 'novale-flood-fill',     kind: 'fill',     toggle: 'flood',     source: 'novale-flood' },
  { id: 'novale-flood-line',     kind: 'line',     toggle: 'flood',     source: 'novale-flood' },
  { id: 'novale-wetland-fill',   kind: 'fill',     toggle: 'wetland',   source: 'novale-wetlands' },
  { id: 'novale-wetland-line',   kind: 'line',     toggle: 'wetland',   source: 'novale-wetlands' },
  { id: 'novale-stream-line',    kind: 'line',     toggle: 'wetland',   source: 'novale-streams' },
  // Geo / hazard
  { id: 'novale-critical-fill',  kind: 'fill',     toggle: 'critical',  source: 'novale-critical' },
  { id: 'novale-critical-line',  kind: 'line',     toggle: 'critical',  source: 'novale-critical' },
  { id: 'novale-contour-line',   kind: 'line',     toggle: 'contours',  source: 'novale-contours' },
  // Soils
  { id: 'novale-soil-fill',      kind: 'fill',     toggle: 'soil',      source: 'novale-soils' },
  { id: 'novale-soil-line',      kind: 'line',     toggle: 'soil',      source: 'novale-soils' },
  { id: 'novale-soil-label',     kind: 'symbol',   toggle: 'soil',      source: 'novale-soils' },
  // Zoning
  { id: 'novale-zoning-fill',    kind: 'fill',     toggle: 'zones',     source: 'novale-zoning' },
  { id: 'novale-zoning-line',    kind: 'line',     toggle: 'zones',     source: 'novale-zoning' },
  { id: 'novale-setbacks-line',  kind: 'line',     toggle: 'zones',     source: 'novale-setbacks', requires: 'setbacks' },
  { id: 'novale-setbacks-label', kind: 'symbol',   toggle: 'zones',     source: 'novale-setbacks', requires: 'setbacks' },
  // Tree canopy raster
  { id: 'novale-canopy-raster',  kind: 'raster',   toggle: 'trees',     source: 'novale-canopy' },
  // Parcel / building (kept on top so they read above fills)
  { id: 'novale-parcel-fill',    kind: 'fill',     toggle: 'boundary',  source: 'novale-parcel' },
  { id: 'novale-parcel-line',    kind: 'line',     toggle: 'boundary',  source: 'novale-parcel' },
  { id: 'novale-building-fill',  kind: 'fill',     toggle: 'structures',source: 'novale-building' },
  { id: 'novale-building-line',  kind: 'line',     toggle: 'structures',source: 'novale-building' },
];

const EMPTY_FC = { type: 'FeatureCollection', features: [] };

const MapboxBase = ({
  lat, lng, token, zoom = 19,
  parcel = null, building = null,
  zoning = null, setbacks = null,
  contours = null, wetlands = null, streams = null,
  criticalAreas = null, flood = null, soils = null,
  treeCanopyBbox = null,
  canopyUrl = null,
  layers = {},
  highlight = null,
}) => {
  const containerRef = React.useRef(null);
  const mapRef = React.useRef(null);
  const [styleReady, setStyleReady] = React.useState(false);
  // Tracks the canopy image source so we can swap data when bbox changes.
  const canopySourceRef = React.useRef(null);

  React.useEffect(() => {
    if (!containerRef.current || !window.mapboxgl) return;
    window.mapboxgl.accessToken = token;
    const map = new window.mapboxgl.Map({
      container: containerRef.current,
      style: 'mapbox://styles/mapbox/satellite-v9',
      center: [lng, lat],
      zoom,
      bearing: 0,
      pitch: 0,
      attributionControl: false,
      interactive: false,
      dragRotate: false,
      doubleClickZoom: false,
      scrollZoom: false,
      boxZoom: false,
      keyboard: false,
      touchZoomRotate: false,
    });
    mapRef.current = map;
    map.on('load', () => {
      // Warm "Blossom Design Base Plan" treatment on the satellite raster —
      // kills the blue/green cast without touching our overlays.
      try {
        map.setPaintProperty('satellite', 'raster-saturation', -0.28);
        map.setPaintProperty('satellite', 'raster-hue-rotate', -10);
        map.setPaintProperty('satellite', 'raster-brightness-min', 0.05);
        map.setPaintProperty('satellite', 'raster-contrast', -0.04);
      } catch (_) { /* layer name varies between style versions */ }

      // Vector sources — registered empty up front; data is pushed in by the
      // setData effects below.
      const vectorSources = [
        'novale-parcel', 'novale-building',
        'novale-zoning', 'novale-setbacks',
        'novale-contours',
        'novale-wetlands', 'novale-streams',
        'novale-critical',
        'novale-flood',
        'novale-soils',
      ];
      for (const id of vectorSources) {
        map.addSource(id, { type: 'geojson', data: EMPTY_FC });
      }

      /* ---- Hydro (lowest, painted first so other layers sit on top) ---- */
      // Flood — soft cyan fill
      map.addLayer({
        id: 'novale-flood-fill', type: 'fill', source: 'novale-flood',
        paint: { 'fill-color': '#3F6E73', 'fill-opacity': 0.18 },
        layout: { visibility: 'none' },
      });
      map.addLayer({
        id: 'novale-flood-line', type: 'line', source: 'novale-flood',
        paint: { 'line-color': '#3F6E73', 'line-width': 1.2, 'line-dasharray': [3, 2] },
        layout: { visibility: 'none' },
      });
      // Wetlands — teal dashed
      map.addLayer({
        id: 'novale-wetland-fill', type: 'fill', source: 'novale-wetlands',
        paint: { 'fill-color': '#3F6E73', 'fill-opacity': 0.28 },
        layout: { visibility: 'none' },
      });
      map.addLayer({
        id: 'novale-wetland-line', type: 'line', source: 'novale-wetlands',
        paint: { 'line-color': '#3F6E73', 'line-width': 1.4, 'line-dasharray': [4, 3] },
        layout: { visibility: 'none' },
      });
      // Streams — blue solid
      map.addLayer({
        id: 'novale-stream-line', type: 'line', source: 'novale-streams',
        paint: { 'line-color': '#2A6D86', 'line-width': 1.6 },
        layout: { visibility: 'none' },
      });

      /* ---- Geo / hazard ---- */
      map.addLayer({
        id: 'novale-critical-fill', type: 'fill', source: 'novale-critical',
        paint: { 'fill-color': '#B45309', 'fill-opacity': 0.16 },
        layout: { visibility: 'none' },
      });
      map.addLayer({
        id: 'novale-critical-line', type: 'line', source: 'novale-critical',
        paint: { 'line-color': '#B45309', 'line-width': 1.1, 'line-dasharray': [2, 2] },
        layout: { visibility: 'none' },
      });
      // Contours — thin warm brown
      map.addLayer({
        id: 'novale-contour-line', type: 'line', source: 'novale-contours',
        paint: {
          'line-color': '#7C5A3A',
          'line-width': ['interpolate', ['linear'], ['zoom'], 16, 0.4, 19, 0.9, 21, 1.2],
          'line-opacity': 0.55,
        },
        layout: { visibility: 'none' },
      });

      /* ---- Soils ---- */
      map.addLayer({
        id: 'novale-soil-fill', type: 'fill', source: 'novale-soils',
        paint: { 'fill-color': '#B0BB7E', 'fill-opacity': 0.10 },
        layout: { visibility: 'none' },
      });
      map.addLayer({
        id: 'novale-soil-line', type: 'line', source: 'novale-soils',
        paint: { 'line-color': '#7C5A3A', 'line-width': 0.8, 'line-opacity': 0.65, 'line-dasharray': [3, 2] },
        layout: { visibility: 'none' },
      });
      // Soil label — render NRCS muname in its original NRCS case (e.g.
      // "Pastik loam, 0 to 30 percent slopes"). Dropping text-transform here
      // because the NRCS canonical casing is more readable than upper-snake
      // and lets users sanity-check the label against soilmap.usda.gov.
      map.addLayer({
        id: 'novale-soil-label', type: 'symbol', source: 'novale-soils',
        layout: {
          'text-field': ['get', 'muname'],
          'text-size': 10,
          'text-letter-spacing': 0.04,
          'symbol-placement': 'point',
          visibility: 'none',
        },
        paint: {
          'text-color': '#7C5A3A',
          'text-halo-color': 'rgba(250,245,236,0.85)',
          'text-halo-width': 1,
        },
      });

      /* ---- Zoning + setbacks ---- */
      map.addLayer({
        id: 'novale-zoning-fill', type: 'fill', source: 'novale-zoning',
        paint: { 'fill-color': '#7C5A3A', 'fill-opacity': 0.08 },
        layout: { visibility: 'none' },
      });
      map.addLayer({
        id: 'novale-zoning-line', type: 'line', source: 'novale-zoning',
        paint: { 'line-color': '#7C5A3A', 'line-width': 1.3, 'line-opacity': 0.85 },
        layout: { visibility: 'none' },
      });
      // Setback line — muted advisory feel, distinct from the orange-dashed
      // parcel boundary so users don't read it as a second property edge.
      map.addLayer({
        id: 'novale-setbacks-line', type: 'line', source: 'novale-setbacks',
        paint: {
          'line-color': '#7C5A3A',
          'line-width': 1,
          'line-dasharray': [2, 4],
          'line-opacity': 0.7,
        },
        layout: { visibility: 'none' },
      });
      // One small "buildable area" caption at the polygon centroid. Picks up
      // satellite-v9's default font; letter-spacing approximates the mono
      // labels the SVG overlay uses.
      map.addLayer({
        id: 'novale-setbacks-label', type: 'symbol', source: 'novale-setbacks',
        layout: {
          'text-field': 'buildable area',
          'text-size': 10,
          'text-letter-spacing': 0.12,
          'symbol-placement': 'point',
          visibility: 'none',
        },
        paint: {
          'text-color': '#7C5A3A',
          'text-halo-color': 'rgba(250,245,236,0.9)',
          'text-halo-width': 1.2,
          'text-opacity': 0.85,
        },
      });

      /* ---- Tree canopy raster (NLCD) ---- */
      // Image source is added lazily in the canopy-bbox effect below, since
      // its `coordinates` depend on data that arrives asynchronously.

      /* ---- Parcel + building (placed last so they read above fills) ---- */
      map.addLayer({
        id: 'novale-parcel-fill', type: 'fill', source: 'novale-parcel',
        paint: { 'fill-color': '#C25E3E', 'fill-opacity': 0.08 },
        layout: { visibility: 'none' },
      });
      map.addLayer({
        id: 'novale-parcel-line', type: 'line', source: 'novale-parcel',
        paint: {
          'line-color': '#C25E3E',
          'line-width': 3,
          'line-dasharray': [2, 1.4],
          'line-opacity': 0.95,
        },
        layout: { visibility: 'none' },
      });
      map.addLayer({
        id: 'novale-building-fill', type: 'fill', source: 'novale-building',
        paint: { 'fill-color': '#E8D9BE', 'fill-opacity': 0.78 },
        layout: { visibility: 'none' },
      });
      map.addLayer({
        id: 'novale-building-line', type: 'line', source: 'novale-building',
        paint: { 'line-color': '#7C5A3A', 'line-width': 1.8 },
        layout: { visibility: 'none' },
      });

      setStyleReady(true);
    });

    const ro = new ResizeObserver(() => {
      try { map.resize(); } catch (_) {}
    });
    ro.observe(containerRef.current);

    return () => { ro.disconnect(); map.remove(); mapRef.current = null; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Recenter when coords change (geometry-driven framing happens below).
  React.useEffect(() => {
    if (mapRef.current && Number.isFinite(lat) && Number.isFinite(lng)) {
      mapRef.current.jumpTo({ center: [lng, lat], zoom });
    }
  }, [lat, lng, zoom]);

  // Helper: push data into a vector source.
  const pushSource = React.useCallback((id, feature) => {
    if (!styleReady || !mapRef.current) return;
    const fc = !feature
      ? EMPTY_FC
      : feature.type === 'FeatureCollection'
        ? feature
        : { type: 'FeatureCollection', features: [feature] };
    mapRef.current.getSource(id)?.setData(fc);
  }, [styleReady]);

  React.useEffect(() => { pushSource('novale-parcel', parcel); }, [parcel, pushSource]);
  React.useEffect(() => { pushSource('novale-building', building); }, [building, pushSource]);
  React.useEffect(() => { pushSource('novale-zoning', zoning); }, [zoning, pushSource]);
  React.useEffect(() => { pushSource('novale-setbacks', setbacks); }, [setbacks, pushSource]);
  React.useEffect(() => { pushSource('novale-contours', contours); }, [contours, pushSource]);
  React.useEffect(() => { pushSource('novale-wetlands', wetlands); }, [wetlands, pushSource]);
  React.useEffect(() => { pushSource('novale-streams', streams); }, [streams, pushSource]);
  React.useEffect(() => { pushSource('novale-critical', criticalAreas); }, [criticalAreas, pushSource]);
  React.useEffect(() => { pushSource('novale-flood', flood); }, [flood, pushSource]);
  React.useEffect(() => { pushSource('novale-soils', soils); }, [soils, pushSource]);

  // Tree canopy raster — add or update the image source when both a working
  // (probed) URL and a bbox are available. SiteAnalysis runs the URL probe
  // against multiple NLCD endpoints; if all fail, canopyUrl is null and we
  // skip the layer entirely.
  React.useEffect(() => {
    if (!styleReady || !mapRef.current) return;
    const map = mapRef.current;
    if (!treeCanopyBbox || !canopyUrl) {
      // If the URL became unavailable, tear down any prior canopy source.
      if (map.getLayer('novale-canopy-raster')) map.removeLayer('novale-canopy-raster');
      if (map.getSource('novale-canopy')) map.removeSource('novale-canopy');
      canopySourceRef.current = null;
      return;
    }
    const [west, south, east, north] = treeCanopyBbox;
    const corners = [
      [west, north], [east, north], [east, south], [west, south],
    ];
    // Re-encode the URL with the latest bbox so the server crops correctly.
    // (canopyUrl was probed against an initial bbox; once parcel narrows the
    // bbox we want a tighter render.)
    const sized = window.nlcdTreeCanopyImageUrl
      ? window.nlcdTreeCanopyImageUrl(treeCanopyBbox, 1024)
      : canopyUrl;
    if (canopySourceRef.current && map.getSource('novale-canopy')) {
      try {
        map.getSource('novale-canopy').updateImage({ url: sized, coordinates: corners });
        return;
      } catch (_) { /* fall through to re-create */ }
    }
    try {
      if (map.getLayer('novale-canopy-raster')) map.removeLayer('novale-canopy-raster');
      if (map.getSource('novale-canopy')) map.removeSource('novale-canopy');
      map.addSource('novale-canopy', {
        type: 'image',
        url: sized,
        coordinates: corners,
      });
      // Insert canopy below parcel/building outlines so their lines stay readable.
      map.addLayer({
        id: 'novale-canopy-raster',
        type: 'raster',
        source: 'novale-canopy',
        paint: {
          'raster-opacity': 0.6,
          // The exportImage default render is white-to-green. Push it warmer to
          // sit alongside the rest of the Novale palette.
          'raster-saturation': -0.15,
          'raster-hue-rotate': -8,
          'raster-fade-duration': 0,
        },
        layout: { visibility: 'none' },
      }, 'novale-parcel-fill');
      canopySourceRef.current = 'novale-canopy';
      console.log('[novale] canopy: layer added');
    } catch (e) {
      console.warn('[novale] tree canopy raster unavailable', e);
    }
  }, [styleReady, treeCanopyBbox, canopyUrl]);

  // Snap framing to the best available geometry. Parcel > building (expanded) > coords.
  React.useEffect(() => {
    if (!styleReady || !mapRef.current) return;
    const map = mapRef.current;
    if (parcel && window.bboxOfPolygon) {
      const bb = window.bboxOfPolygon(parcel);
      if (bb) {
        try {
          map.fitBounds([[bb[0], bb[1]], [bb[2], bb[3]]], {
            padding: 36, animate: false, maxZoom: 20,
          });
          return;
        } catch (_) {}
      }
    }
    if (building && window.bboxOfPolygon) {
      const bb = window.bboxOfPolygon(building);
      if (bb) {
        const w = bb[2] - bb[0], h = bb[3] - bb[1];
        const pad = Math.max(w, h) * 0.9;
        try {
          map.fitBounds([[bb[0] - pad, bb[1] - pad], [bb[2] + pad, bb[3] + pad]], {
            padding: 36, animate: false, maxZoom: 20,
          });
          return;
        } catch (_) {}
      }
    }
    if (Number.isFinite(lat) && Number.isFinite(lng)) {
      map.jumpTo({ center: [lng, lat], zoom: 18.5 });
    }
  }, [parcel, building, lat, lng, styleReady]);

  // Visibility wiring — for each layer, show only if the toggle is on AND we
  // actually have data for it (no fake fallback).
  React.useEffect(() => {
    if (!styleReady || !mapRef.current) return;
    const map = mapRef.current;
    const hasData = {
      boundary: !!parcel,
      structures: !!building,
      zones: !!zoning,
      setbacks: !!setbacks,
      contours: !!contours?.features?.length,
      wetland: Boolean(wetlands?.features?.length || streams?.features?.length),
      critical: !!criticalAreas?.features?.length,
      flood: !!flood?.features?.length,
      soil: !!soils?.features?.length,
      trees: !!(treeCanopyBbox && canopyUrl),
    };
    for (const def of NOVALE_LAYERS) {
      if (!map.getLayer(def.id)) continue;
      let on = !!layers[def.toggle] && !!hasData[def.toggle];
      // Some layers ride on the same toggle but require their own derived
      // data (e.g. setbacks need the inset polygon, not just a zoning hit).
      if (def.requires) on = on && !!hasData[def.requires];
      map.setLayoutProperty(def.id, 'visibility', on ? 'visible' : 'none');
    }
    // Also handle the lazily-added canopy raster, in case the registry entry
    // was checked before the layer existed.
    if (map.getLayer('novale-canopy-raster')) {
      const on = !!layers.trees && !!treeCanopyBbox && !!canopyUrl;
      map.setLayoutProperty('novale-canopy-raster', 'visibility', on ? 'visible' : 'none');
    }
  }, [
    styleReady, layers,
    parcel, building, zoning, contours, wetlands, streams, criticalAreas,
    flood, soils, treeCanopyBbox, canopyUrl,
  ]);

  // Pulse the active layer during the reveal — emulate the SVG `bm-pulse` vibe.
  React.useEffect(() => {
    if (!styleReady || !mapRef.current) return;
    const map = mapRef.current;
    const PULSE = {
      boundary: { id: 'novale-parcel-line',   prop: 'line-width', base: 3,    bump: 1.4 },
      structures:{ id: 'novale-building-line', prop: 'line-width', base: 1.8, bump: 1.2 },
      wetland:  { id: 'novale-wetland-line',  prop: 'line-width', base: 1.4, bump: 1.2 },
      contours: { id: 'novale-contour-line',  prop: 'line-width', base: 0.9, bump: 0.8 },
      flood:    { id: 'novale-flood-line',    prop: 'line-width', base: 1.2, bump: 1.0 },
      critical: { id: 'novale-critical-line', prop: 'line-width', base: 1.1, bump: 1.1 },
      zones:    { id: 'novale-zoning-line',   prop: 'line-width', base: 1.3, bump: 1.0 },
      soil:     { id: 'novale-soil-line',     prop: 'line-width', base: 0.8, bump: 0.6 },
    };
    for (const [key, p] of Object.entries(PULSE)) {
      if (!map.getLayer(p.id)) continue;
      map.setPaintProperty(p.id, p.prop, highlight === key ? p.base + p.bump : p.base);
    }
  }, [highlight, styleReady]);

  return <div ref={containerRef} className="basemap-stack__map" />;
};

/* ==== Reusable pieces ==== */

const ExistingTree = ({ x, y, r, kind, mode }) => {
  if (mode === 'cad') {
    return (
      <g transform={`translate(${x}, ${y})`}>
        <circle r={r} fill="none" stroke="#1C1917" strokeWidth="1" strokeDasharray="2 2" />
        {Array.from({ length: 8 }).map((_, i) => {
          const a = (i / 8) * Math.PI * 2;
          return <path key={i} d={`M ${Math.cos(a) * r * 0.3} ${Math.sin(a) * r * 0.3} L ${Math.cos(a) * r * 0.9} ${Math.sin(a) * r * 0.9}`} stroke="#1C1917" strokeWidth="0.6" />;
        })}
        <circle r="2" fill="#1C1917" />
      </g>
    );
  }
  const greens = {
    conifer: { canopy: '#3D4421', ring: '#2D331A' },
    maple: { canopy: '#6B7541', ring: '#525B33' },
    oak: { canopy: '#525B33', ring: '#3D4421' },
  };
  const c = greens[kind] || greens.maple;
  return (
    <g transform={`translate(${x}, ${y})`}>
      <circle r={r + 1} fill={c.canopy} fillOpacity="0.25" />
      <circle r={r} fill={c.canopy} fillOpacity={mode === 'render' ? 0.8 : 0.5} stroke={c.ring} strokeWidth="1" />
      {Array.from({ length: 6 }).map((_, i) => {
        const a = (i / 6) * Math.PI * 2 + (kind.length);
        return <circle key={i} cx={Math.cos(a) * r * 0.55} cy={Math.sin(a) * r * 0.55} r={1.5} fill={c.ring} fillOpacity="0.6" />;
      })}
      <circle r="2" fill={c.ring} />
    </g>
  );
};

/* ==== Planting plan — generated design in the back yard ==== */

const PLANT_SYMBOLS = [
  { x: 180, y: 460, r: 22, color: '#3D4421', ring: '#2D331A', label: 'AM', type: 'tree' },
  { x: 720, y: 460, r: 22, color: '#3D4421', ring: '#2D331A', label: 'AM', type: 'tree' },
  { x: 240, y: 475, r: 10, color: '#6B7541', ring: '#525B33', label: 'MO', type: 'shrub' },
  { x: 280, y: 460, r: 10, color: '#6B7541', ring: '#525B33', label: 'MO', type: 'shrub' },
  { x: 660, y: 475, r: 10, color: '#6B7541', ring: '#525B33', label: 'MO', type: 'shrub' },
  { x: 620, y: 460, r: 10, color: '#6B7541', ring: '#525B33', label: 'MO', type: 'shrub' },
  { x: 330, y: 460, r: 6, color: '#D97757', ring: '#9F4A30', label: 'EC', type: 'perennial' },
  { x: 345, y: 478, r: 6, color: '#D97757', ring: '#9F4A30', label: 'EC', type: 'perennial' },
  { x: 560, y: 460, r: 6, color: '#D97757', ring: '#9F4A30', label: 'EC', type: 'perennial' },
  { x: 575, y: 478, r: 6, color: '#D97757', ring: '#9F4A30', label: 'EC', type: 'perennial' },
  { x: 380, y: 492, r: 7, color: '#FDE68A', ring: '#92400E', label: 'PA', type: 'grass' },
  { x: 400, y: 480, r: 7, color: '#FDE68A', ring: '#92400E', label: 'PA', type: 'grass' },
  { x: 500, y: 480, r: 7, color: '#FDE68A', ring: '#92400E', label: 'PA', type: 'grass' },
  { x: 520, y: 492, r: 7, color: '#FDE68A', ring: '#92400E', label: 'PA', type: 'grass' },
  { x: 440, y: 470, r: 5, color: '#B0BB7E', ring: '#6B7541', label: 'TH', type: 'ground' },
  { x: 460, y: 485, r: 5, color: '#B0BB7E', ring: '#6B7541', label: 'TH', type: 'ground' },
  { x: 420, y: 490, r: 5, color: '#B0BB7E', ring: '#6B7541', label: 'TH', type: 'ground' },
];

const PlantingPlan = ({ mode }) => {
  return (
    <g className="bm-plants-in">
      <rect x="340" y="430" width="220" height="110" fill={mode === 'cad' ? '#fff' : '#E8D9BE'} stroke={mode === 'cad' ? '#1C1917' : '#7C5A3A'} strokeWidth={mode === 'cad' ? 1 : 1.2} />
      {mode === 'cad' && (
        <g opacity="0.45">
          {Array.from({length: 6}).map((_, i) => (
            <line key={i} x1="340" y1={430 + i*22} x2="560" y2={430 + i*22} stroke="#1C1917" strokeWidth="0.3" />
          ))}
          {Array.from({length: 11}).map((_, i) => (
            <line key={i} x1={340 + i*22} y1="430" x2={340 + i*22} y2="540" stroke="#1C1917" strokeWidth="0.3" />
          ))}
        </g>
      )}
      <circle cx="450" cy="495" r="14" fill={mode === 'cad' ? '#fff' : '#7C3923'} stroke={mode === 'cad' ? '#1C1917' : '#5A2818'} strokeWidth={mode === 'cad' ? 1 : 1.5} />
      <circle cx="450" cy="495" r="8" fill={mode === 'cad' ? 'none' : '#D97757'} stroke={mode === 'cad' ? '#1C1917' : 'none'} strokeWidth="0.5" />

      <path d="M 150 440 Q 170 430, 200 438 Q 240 444, 270 436 Q 310 430, 340 444 L 340 520 L 150 520 Z"
        fill={mode === 'cad' ? 'none' : '#6B7541'} fillOpacity="0.18"
        stroke={mode === 'cad' ? '#1C1917' : '#525B33'} strokeWidth={mode === 'cad' ? 0.8 : 1.2} strokeDasharray={mode === 'cad' ? '3 2' : '0'} />
      <path d="M 560 444 Q 590 430, 620 438 Q 660 444, 690 436 Q 720 430, 750 440 L 750 520 L 560 520 Z"
        fill={mode === 'cad' ? 'none' : '#6B7541'} fillOpacity="0.18"
        stroke={mode === 'cad' ? '#1C1917' : '#525B33'} strokeWidth={mode === 'cad' ? 0.8 : 1.2} strokeDasharray={mode === 'cad' ? '3 2' : '0'} />

      {PLANT_SYMBOLS.map((p, i) => (
        <PlantSymbol key={i} {...p} mode={mode} delay={i * 40} />
      ))}

      {mode !== 'render' && (
        <>
          <text x="450" y="518" fontFamily="Inter" fontSize="9" fontWeight="600" fill={mode === 'cad' ? '#1C1917' : '#3D4421'} textAnchor="middle" letterSpacing="0.1em">FLAGSTONE PATIO · 220 ft²</text>
          <text x="450" y="535" fontFamily="Inter" fontSize="9" fontWeight="500" fill={mode === 'cad' ? '#1C1917' : '#7C3923'} textAnchor="middle">fire pit</text>
        </>
      )}
    </g>
  );
};

const PlantSymbol = ({ x, y, r, color, ring, label, mode, type, delay }) => {
  const isCad = mode === 'cad';
  return (
    <g transform={`translate(${x}, ${y})`} style={{ animation: `plantPop 460ms ${delay}ms var(--ease-standard) both` }}>
      {isCad ? (
        <>
          <circle r={r} fill="none" stroke="#1C1917" strokeWidth="1" />
          {Array.from({ length: Math.max(4, Math.round(r)) }).map((_, i) => {
            const a = (i / Math.max(4, Math.round(r))) * Math.PI * 2;
            return <line key={i} x1="0" y1="0" x2={Math.cos(a) * r} y2={Math.sin(a) * r} stroke="#1C1917" strokeWidth="0.4" />;
          })}
          <circle r="1.2" fill="#1C1917" />
          {r >= 8 && <text y="1" fontFamily="Geist Mono" fontSize="5" fill="#1C1917" textAnchor="middle">{label}</text>}
        </>
      ) : (
        <>
          <circle r={r + 1.5} fill={color} fillOpacity="0.2" />
          <circle r={r} fill={color} fillOpacity={mode === 'render' ? 0.95 : 0.65} stroke={ring} strokeWidth="0.8" />
          {type === 'perennial' && <circle r={r * 0.4} fill={ring} fillOpacity="0.8" />}
          {type === 'grass' && Array.from({ length: 5 }).map((_, i) => (
            <line key={i} x1="0" y1="0" x2={(i - 2) * 1.5} y2={-r * 0.9} stroke={ring} strokeWidth="0.8" />
          ))}
        </>
      )}
    </g>
  );
};

Object.assign(window, { BaseMap, MapboxBase });
