// PrecinctMap — Shelby County choropleth using Enhanced Voting's own
// Mapbox vector tileset. Same boundaries the SCEC publishes on their
// official results page. We apply data-driven coloring via Mapbox
// `feature-state`, joined by feature.id (mirrors EV's own approach):
// query rendered tile features, look up the app precinct id from
// EV_PRECINCT_LOOKUP, and call setFeatureState with the winner color.

const MAPBOX_TOKEN = 'pk.eyJ1IjoiZW5oYW5jZWR2b3RpbmciLCJhIjoiY2wyeXltcjhhMGhvdDNqb3cyemF5cTBzMiJ9._qMLB2upmKWdNxHANncxiA';
const TILESET = 'enhancedvoting.Tennessee-Shelby-2024';
const SHELBY_BBOX = [[-90.318604, 34.994004], [-89.626465, 35.415915]];

// Per-candidate base hue → light/dark ramp on margin (% of votes over
// runner-up). Hues spaced for max separation among the four
// candidates that actually win precincts (Lowery, Smiley, Feagins,
// Kuhn): teal vs orange vs purple vs green.
const CAND_PALETTE = {
  lowery:  { h: 188, name: 'Lowery'  },  // teal
  smiley:  { h: 30,  name: 'Smiley'  },  // amber
  feagins: { h: 280, name: 'Feagins' },  // purple
  kuhn:    { h: 130, name: 'Kuhn'    },  // green
  burgess: { h: 340, name: 'Burgess' },  // magenta
  collins: { h: 220, name: 'Collins' },  // navy-blue
  qualls:  { h: 50,  name: 'Qualls'  },  // mustard
};

function hslColor(h, marginPct) {
  const t = Math.max(0, Math.min(1, marginPct / 30));
  const s = 28 + t * 50;
  const l = 86 - t * 38;
  // Comma-separated HSL — Mapbox GL doesn't parse the modern
  // space-separated CSS Color Level 4 syntax and falls back to black.
  return `hsl(${h}, ${s.toFixed(0)}%, ${l.toFixed(0)}%)`;
}

function PrecinctMap({ precincts, store, visibleCands }) {
  const containerRef = React.useRef(null);
  const mapRef = React.useRef(null);
  const popupRef = React.useRef(null);
  const hoverIdRef = React.useRef(null);
  const [ready, setReady] = React.useState(false);
  const [error, setError] = React.useState(null);

  const resultFor = React.useCallback((pid) => {
    return (window.ED_RESULTS_2026 || {})[pid] || null;
  }, []);

  const winnerInfoFor = React.useCallback((pid) => {
    const r = resultFor(pid);
    if (!r) return null;
    const cands = Object.keys(CAND_PALETTE)
      .map(id => ({ id, votes: r[id] || 0 }))
      .sort((a, b) => b.votes - a.votes);
    const top = cands[0], runner = cands[1];
    if (!top || top.votes === 0) return null;
    const total = r.totalBallots || cands.reduce((s, c) => s + c.votes, 0);
    const marginPct = total > 0 ? ((top.votes - (runner ? runner.votes : 0)) / total) * 100 : 0;
    return { winnerId: top.id, marginPct, total, cands };
  }, [resultFor]);

  // Iterate every feature in the source layer (across loaded tiles) and
  // apply per-feature state for the winner color. Coalesce against a
  // light-grey fallback in the paint expression so unloaded tiles still
  // render something instead of going white.
  const applyFeatureStates = React.useCallback(() => {
    const map = mapRef.current;
    if (!map) return;
    const lookup = window.EV_PRECINCT_LOOKUP || {};
    let features;
    try {
      features = map.querySourceFeatures('shelby', { sourceLayer: 'precincts' });
    } catch (e) {
      return; // source not yet loaded
    }
    const seen = new Set();
    for (const f of features) {
      if (f.id == null || seen.has(f.id)) continue;
      seen.add(f.id);
      const appId = lookup[String(f.id)];
      if (!appId) continue;
      const info = winnerInfoFor(appId);
      const color = info
        ? hslColor(CAND_PALETTE[info.winnerId].h, info.marginPct)
        : '#dde3ef';
      map.setFeatureState(
        { source: 'shelby', sourceLayer: 'precincts', id: f.id },
        { fillColor: color, appId, winnerId: info ? info.winnerId : null }
      );
    }
  }, [winnerInfoFor]);

  // Init map (once).
  React.useEffect(() => {
    if (!containerRef.current || mapRef.current) return;
    const mb = window.mapboxgl;
    if (!mb) { setError('Mapbox GL failed to load'); return; }

    mb.accessToken = MAPBOX_TOKEN;
    const map = new mb.Map({
      container: containerRef.current,
      style: 'mapbox://styles/mapbox/light-v11',
      bounds: SHELBY_BBOX,
      fitBoundsOptions: { padding: 24, animate: false },
      attributionControl: { compact: true },
      // Box the camera to Shelby — without this, fitBounds on a wide
      // viewport can resolve to a way-too-low zoom that shows AR/MS too.
      maxBounds: [[-90.7, 34.7], [-89.2, 35.7]],
      minZoom: 8.5,
      maxZoom: 13,
    });
    mapRef.current = map;

    map.addControl(new mb.NavigationControl({ showCompass: false }), 'top-right');

    // Re-fit on container resize. Wide-viewport / tab-switch races can
    // leave the camera way out before the container reaches its final
    // dimensions; ResizeObserver corrects after layout settles.
    let ro;
    if (typeof ResizeObserver !== 'undefined') {
      ro = new ResizeObserver(() => {
        map.resize();
        map.fitBounds(SHELBY_BBOX, { padding: 24, animate: false });
      });
      ro.observe(containerRef.current);
    }

    map.on('load', () => {
      map.addSource('shelby', {
        type: 'vector',
        url: `mapbox://${TILESET}`,
      });
      map.addLayer({
        id: 'shelby-fill',
        type: 'fill',
        source: 'shelby',
        'source-layer': 'precincts',
        paint: {
          'fill-color': ['coalesce', ['feature-state', 'fillColor'], '#e6ecf3'],
          'fill-opacity': [
            'case',
            ['boolean', ['feature-state', 'hover'], false], 0.92,
            0.78,
          ],
        },
      });
      map.addLayer({
        id: 'shelby-line',
        type: 'line',
        source: 'shelby',
        'source-layer': 'precincts',
        paint: {
          'line-color': '#0A1530',
          'line-width': [
            'case',
            ['boolean', ['feature-state', 'hover'], false], 2.2,
            0.4,
          ],
          'line-opacity': [
            'case',
            ['boolean', ['feature-state', 'hover'], false], 0.95,
            0.55,
          ],
        },
      });

      map.on('mousemove', 'shelby-fill', (e) => {
        if (!e.features.length) return;
        map.getCanvas().style.cursor = 'pointer';
        const fid = e.features[0].id;
        if (fid == null) return;
        if (hoverIdRef.current != null && hoverIdRef.current !== fid) {
          map.setFeatureState(
            { source: 'shelby', sourceLayer: 'precincts', id: hoverIdRef.current },
            { hover: false }
          );
        }
        hoverIdRef.current = fid;
        map.setFeatureState(
          { source: 'shelby', sourceLayer: 'precincts', id: fid },
          { hover: true }
        );
      });
      map.on('mouseleave', 'shelby-fill', () => {
        map.getCanvas().style.cursor = '';
        if (hoverIdRef.current != null) {
          map.setFeatureState(
            { source: 'shelby', sourceLayer: 'precincts', id: hoverIdRef.current },
            { hover: false }
          );
          hoverIdRef.current = null;
        }
      });

      map.on('click', 'shelby-fill', (e) => {
        const f = e.features[0];
        if (!f || f.id == null) return;
        const lookup = window.EV_PRECINCT_LOOKUP || {};
        const appId = lookup[String(f.id)];
        if (!appId) return;
        if (popupRef.current) popupRef.current.remove();
        // anchor:'auto' lets Mapbox flip the popup to whichever side
        // keeps it inside the map. Constrain max-width to viewport so
        // it doesn't overflow on narrow phones.
        const maxW = Math.min(320, (containerRef.current?.clientWidth || 320) - 32);
        popupRef.current = new mb.Popup({
          maxWidth: maxW + 'px',
          closeButton: true,
          anchor: 'auto',
          offset: 12,
          className: 'pmp-popup',
        })
          .setLngLat(e.lngLat)
          .setHTML(popupHtml(appId, precincts))
          .addTo(map);
      });

      // Apply state once tiles are loaded; reapply as new tiles arrive
      // (panning/zooming) so newly-rendered features pick up the join.
      map.on('sourcedata', (e) => {
        if (e.sourceId === 'shelby' && e.isSourceLoaded) applyFeatureStates();
      });
      map.on('idle', applyFeatureStates);

      setReady(true);
    });

    map.on('error', (e) => {
      // Token-restriction or tile-load errors land here; surface to UI.
      // eslint-disable-next-line no-console
      console.warn('mapbox error', e && e.error);
    });

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

  // Reapply feature-state when results change.
  React.useEffect(() => {
    if (!ready) return;
    applyFeatureStates();
  }, [applyFeatureStates, ready]);

  // Popup HTML builder. Includes the per-precinct vote-method split
  // (EV / ED / Abs) sourced from VOTE_METHOD_2026, plus a write-in row
  // at the bottom for completeness.
  const popupHtml = React.useCallback((appId, precinctsList) => {
    const r = resultFor(appId) || {};
    const info = winnerInfoFor(appId);
    const cands = Object.keys(CAND_PALETTE)
      .map(id => ({ id, name: CAND_PALETTE[id].name, votes: r[id] || 0, isClient: id === 'lowery' }))
      .sort((a, b) => b.votes - a.votes);
    const writeInVotes = r.writeIn || 0;
    const denom = r.totalBallots || cands.reduce((s, c) => s + c.votes, 0) + writeInVotes;
    const winnerId = info ? info.winnerId : null;
    const candRows = cands.map(c => {
      const pct = denom > 0 ? (c.votes / denom) * 100 : 0;
      const win = c.id === winnerId ? '<span class="pmp-star">★</span>' : '';
      return `<tr class="${c.isClient ? 'pmp-row-lowery' : ''}${c.id === winnerId ? ' pmp-row-winner' : ''}">
        <td>${win}${c.name}</td>
        <td class="num">${c.votes.toLocaleString()}</td>
        <td class="num">${pct.toFixed(1)}%</td>
      </tr>`;
    }).join('');
    const writeInRow = writeInVotes > 0
      ? `<tr class="pmp-row-writein"><td>Write-in</td><td class="num">${writeInVotes.toLocaleString()}</td><td class="num">${denom > 0 ? ((writeInVotes/denom)*100).toFixed(1) : '0.0'}%</td></tr>`
      : '';
    const vm = (window.VOTE_METHOD_2026 || {})[appId];
    const vmLine = vm
      ? `<div class="pmp-vm">EV ${vm.ev.total.toLocaleString()} · ED ${vm.ed.total.toLocaleString()} · Abs ${vm.abs.total.toLocaleString()}</div>`
      : '';
    const loc = (precinctsList || []).find(p => p.id === appId);
    const winnerName = winnerId ? CAND_PALETTE[winnerId].name : 'No data';
    const margin = info ? info.marginPct : null;
    return `
      <div class="pmp">
        <div class="pmp-head">
          <div class="pmp-name">${appId}${loc && loc.location ? ' · ' + loc.location : ''}</div>
          ${loc && loc.city ? `<div class="pmp-ids">${loc.city}</div>` : ''}
        </div>
        <div class="pmp-share">
          ${winnerId
            ? `Winner: <b>${winnerName}</b> · margin <b>${margin.toFixed(1)}%</b>`
            : 'No data'}
        </div>
        ${vmLine}
        <table class="pmp-table">
          <thead><tr><th>Candidate</th><th class="num">Votes</th><th class="num">%</th></tr></thead>
          <tbody>${candRows}${writeInRow}</tbody>
          <tfoot><tr><td>Total</td><td class="num">${denom.toLocaleString()}</td><td></td></tr></tfoot>
        </table>
      </div>
    `;
  }, [resultFor, winnerInfoFor]);

  return (
    <div className="precinct-map-wrap">
      <div ref={containerRef} className="precinct-map" aria-label="Precinct results map" />
      {error && <div className="precinct-map-error">{error}</div>}
      <div className="precinct-map-legend" aria-label="Legend">
        <div className="pml-title">Precinct Winner · Margin</div>
        {Object.keys(CAND_PALETTE).map(id => {
          const { h, name } = CAND_PALETTE[id];
          return (
            <div className="pml-row" key={id}>
              <span
                className="pml-swatch"
                style={{ background: `linear-gradient(90deg, ${hslColor(h, 1)}, ${hslColor(h, 30)})` }}
              />
              <span>{name}</span>
            </div>
          );
        })}
        <div className="pml-foot">light = close · dark = blowout</div>
      </div>
    </div>
  );
}

window.PrecinctMap = PrecinctMap;
