// USA Choropleth — real state geography via us-atlas TopoJSON + d3-geo.
// Continuous diverging palette + distinct "no data" treatment.
{
const { STATES, scoreFor, lensMetaFor } = window.PL_DATA;

// Five-stop continuous diverging palette: rust → terracotta → slate → jade → teal.
// Continuous (not bucketed) so subtle differences between states are visible.
const PALETTE_STOPS = [
  { at: 0,   c: "#5a2a1f" },  // rust       — most restrictive
  { at: 25,  c: "#b85a4a" },  // terracotta
  { at: 50,  c: "#475569" },  // slate      — neutral
  { at: 75,  c: "#6b9b94" },  // jade
  { at: 100, c: "#2a6b66" },  // teal       — most protective
];
const _palette = window.d3.scaleLinear()
  .domain(PALETTE_STOPS.map(s => s.at))
  .range(PALETTE_STOPS.map(s => s.c))
  .interpolate(window.d3.interpolateRgb)
  .clamp(true);

function colorForScore(s) {
  return { color: _palette(s), text: "#fff" };
}

// A state has "real data" for a lens only if at least one bill was scored
// against that lens for that state. Without this distinction, unscored states
// would look identical to scored-as-neutral states.
function stateHasData(code, lensId) {
  if (!lensMetaFor) return true;
  const meta = lensMetaFor(code, lensId);
  return !!(meta && meta.bill_count > 0);
}

window.PL_COLOR = colorForScore;
window.PL_HAS_DATA = stateHasData;

// Map state-name (as used in us-atlas) → 2-letter code we score with.
const NAME_TO_CODE = {
  "Alabama":"AL","Alaska":"AK","Arizona":"AZ","Arkansas":"AR","California":"CA",
  "Colorado":"CO","Connecticut":"CT","Delaware":"DE","Florida":"FL","Georgia":"GA",
  "Hawaii":"HI","Idaho":"ID","Illinois":"IL","Indiana":"IN","Iowa":"IA",
  "Kansas":"KS","Kentucky":"KY","Louisiana":"LA","Maine":"ME","Maryland":"MD",
  "Massachusetts":"MA","Michigan":"MI","Minnesota":"MN","Mississippi":"MS","Missouri":"MO",
  "Montana":"MT","Nebraska":"NE","Nevada":"NV","New Hampshire":"NH","New Jersey":"NJ",
  "New Mexico":"NM","New York":"NY","North Carolina":"NC","North Dakota":"ND","Ohio":"OH",
  "Oklahoma":"OK","Oregon":"OR","Pennsylvania":"PA","Rhode Island":"RI","South Carolina":"SC",
  "South Dakota":"SD","Tennessee":"TN","Texas":"TX","Utah":"UT","Vermont":"VT",
  "Virginia":"VA","Washington":"WA","West Virginia":"WV","Wisconsin":"WI","Wyoming":"WY",
  "District of Columbia":"DC",
};

// Approx label centroids for state-abbr overlays (lon,lat) — used in projected space at draw time.
// We compute centroids from the geometry instead of hard-coding.

// Single shared promise for the topojson fetch.
let _atlasPromise = null;
function loadAtlas() {
  if (!_atlasPromise) {
    _atlasPromise = fetch("https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json")
      .then(r => r.json());
  }
  return _atlasPromise;
}

function Choropleth({ lensId, mode = "passed", onSelect, activeState, showLabels = "always" }) {
  const [atlas, setAtlas] = React.useState(null);
  const [hover, setHover] = React.useState(null); // {code, name, score, x, y}
  const [transform, setTransform] = React.useState({ k: 1, x: 0, y: 0 });
  const wrapRef = React.useRef(null);
  const svgRef = React.useRef(null);
  const zoomBehaviorRef = React.useRef(null);

  React.useEffect(() => {
    loadAtlas().then(setAtlas).catch(err => {
      console.warn("atlas load failed", err);
    });
  }, []);

  // Wire d3 zoom + pan once the atlas + svg are ready
  React.useEffect(() => {
    if (!atlas || !svgRef.current) return;
    const svg = window.d3.select(svgRef.current);
    const z = window.d3.zoom()
      .scaleExtent([1, 6])
      .on("zoom", (e) => setTransform({ k: e.transform.k, x: e.transform.x, y: e.transform.y }));
    zoomBehaviorRef.current = z;
    svg.call(z);
    return () => svg.on(".zoom", null);
  }, [atlas]);

  const resetZoom = () => {
    if (svgRef.current && zoomBehaviorRef.current) {
      window.d3.select(svgRef.current).transition().duration(280)
        .call(zoomBehaviorRef.current.transform, window.d3.zoomIdentity);
    }
  };

  if (!atlas) {
    return (
      <div style={{
        aspectRatio: "1.6 / 1", width: "100%",
        background: "var(--bg-3)",
        display: "flex", alignItems: "center", justifyContent: "center",
        fontFamily: "var(--mono)", fontSize: 11, color: "var(--ink-3)",
        letterSpacing: "0.08em", textTransform: "uppercase",
      }}>
        Loading geography…
      </div>
    );
  }

  const W = 975, H = 610;
  const fc = window.topojson.feature(atlas, atlas.objects.states);
  const states = fc.features;
  const projection = window.d3.geoAlbersUsa().fitSize([W, H], fc);
  const path = window.d3.geoPath(projection);

  // Compute label centroids for non-tiny states.
  const centroidByCode = {};
  states.forEach(f => {
    const code = NAME_TO_CODE[f.properties.name];
    if (!code) return;
    const c = path.centroid(f);
    const a = path.area(f);
    centroidByCode[code] = { x: c[0], y: c[1], area: a };
  });

  return (
    <div ref={wrapRef} style={{ position: "relative", width: "100%", height: "100%" }}>
      <svg
        ref={svgRef}
        viewBox={`0 0 ${W} ${H}`}
        preserveAspectRatio="xMidYMid meet"
        style={{ width: "100%", height: "100%", display: "block",
          cursor: "grab",
          filter: "drop-shadow(0 1px 0 rgba(0,0,0,0.04))" }}
      >
        <defs>
          {/* subtle paper grain noise */}
          <filter id="pl-grain" x="0" y="0" width="100%" height="100%">
            <feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="3" />
            <feColorMatrix values="0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.06 0" />
            <feComposite in2="SourceGraphic" operator="in" />
          </filter>
          <filter id="pl-hover" x="-10%" y="-10%" width="120%" height="120%">
            <feDropShadow dx="0" dy="2" stdDeviation="3" floodColor="#000" floodOpacity="0.18"/>
          </filter>
          {/* Hatched fill for "no data" states — visually distinct from any scored color. */}
          <pattern id="pl-nodata" patternUnits="userSpaceOnUse" width="6" height="6" patternTransform="rotate(45)">
            <rect width="6" height="6" fill="var(--bg-3)" />
            <line x1="0" y1="0" x2="0" y2="6" stroke="var(--ink-4)" strokeWidth="0.6" opacity="0.5" />
          </pattern>
        </defs>

        {/* zoom transform group — wraps everything so zoom/pan affects all layers together */}
        <g transform={`translate(${transform.x}, ${transform.y}) scale(${transform.k})`}>

        {/* base fills */}
        <g>
          {states.map(f => {
            const code = NAME_TO_CODE[f.properties.name];
            if (!code) return null;
            const hasData = window.PL_HAS_DATA ? window.PL_HAS_DATA(code, lensId) : true;
            const score = scoreFor(code, lensId, mode);
            const pal = colorForScore(score);
            const isActive = activeState === code;
            const isHover = hover && hover.code === code;
            const fillColor = hasData ? pal.color : "url(#pl-nodata)";
            return (
              <path
                key={code}
                d={path(f)}
                fill={fillColor}
                stroke={isActive ? "var(--ink)" : "var(--bg-3)"}
                strokeWidth={isActive ? 2.4 : 1.2}
                style={{
                  cursor: hasData ? "pointer" : "default",
                  transition: "transform 140ms ease, filter 140ms ease, stroke-width 120ms ease",
                  transformOrigin: "center",
                  transformBox: "fill-box",
                  transform: isHover ? "scale(1.012)" : "scale(1)",
                  filter: isHover || isActive ? "url(#pl-hover)" : "none",
                  opacity: hasData ? 1 : 0.65,
                }}
                onMouseEnter={(e) => {
                  const rect = wrapRef.current.getBoundingClientRect();
                  setHover({
                    code, name: f.properties.name, score, hasData,
                    x: e.clientX - rect.left, y: e.clientY - rect.top,
                  });
                }}
                onMouseMove={(e) => {
                  const rect = wrapRef.current.getBoundingClientRect();
                  setHover(h => h ? { ...h, x: e.clientX - rect.left, y: e.clientY - rect.top } : h);
                }}
                onMouseLeave={() => setHover(null)}
                onClick={() => hasData && onSelect && onSelect(code)}
              >
                <title>{f.properties.name} · {hasData ? Math.round(score) : "no data yet"}</title>
              </path>
            );
          })}
        </g>

        {/* paper grain layer over fills */}
        <g style={{ pointerEvents: "none", mixBlendMode: "multiply" }}>
          {states.map(f => {
            const code = NAME_TO_CODE[f.properties.name];
            if (!code) return null;
            return (
              <path key={code + "-g"} d={path(f)} fill="#000" opacity="0.07" filter="url(#pl-grain)" />
            );
          })}
        </g>

        {/* state-abbr labels for non-tiny states */}
        {showLabels !== "never" && (
          <g style={{ pointerEvents: "none" }}>
            {Object.entries(centroidByCode).map(([code, c]) => {
              if (c.area < 320) return null; // skip RI/DE/CT — hover only
              const showThis = showLabels === "always" ||
                (showLabels === "hover" && hover && hover.code === code) ||
                (activeState === code);
              if (!showThis) return null;
              return (
                <text key={code + "-lbl"} x={c.x} y={c.y}
                  textAnchor="middle" dominantBaseline="central"
                  style={{
                    fontFamily: "JetBrains Mono, ui-monospace, monospace",
                    fontSize: 11, fontWeight: 600, fill: "rgba(255,255,255,0.92)",
                    paintOrder: "stroke",
                    stroke: "rgba(0,0,0,0.18)", strokeWidth: 0.6,
                  }}>{code}</text>
              );
            })}
          </g>
        )}

        </g>{/* end zoom transform group */}
      </svg>

      {/* Zoom controls */}
      <div style={{
        position: "absolute", bottom: 16, left: 16, zIndex: 5,
        display: "flex", flexDirection: "column", gap: 1,
        background: "var(--bg)", border: "1px solid var(--rule)",
      }}>
        <button onClick={(e) => {
          e.stopPropagation();
          window.d3.select(svgRef.current).transition().duration(200)
            .call(zoomBehaviorRef.current.scaleBy, 1.5);
        }} className="pl-zoom-btn" title="Zoom in">+</button>
        <button onClick={(e) => {
          e.stopPropagation();
          window.d3.select(svgRef.current).transition().duration(200)
            .call(zoomBehaviorRef.current.scaleBy, 0.667);
        }} className="pl-zoom-btn" title="Zoom out">−</button>
        <button onClick={resetZoom} className="pl-zoom-btn" style={{ fontSize: 10 }} title="Reset">⤓</button>
      </div>

      {/* Tooltip */}
      {hover && (
        <div style={{
          position: "absolute",
          left: Math.min(hover.x + 14, (wrapRef.current?.clientWidth || 600) - 220),
          top: Math.max(hover.y - 12, 8),
          pointerEvents: "none",
          background: "var(--ink)", color: "var(--bg)",
          padding: "10px 12px",
          fontFamily: "var(--mono)", fontSize: 11,
          letterSpacing: "0.04em",
          minWidth: 200,
          boxShadow: "0 8px 24px rgba(0,0,0,0.18)",
          zIndex: 5,
        }}>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
            <div style={{ fontFamily: "var(--display)", fontSize: 18, fontWeight: 600, letterSpacing: "-0.01em" }}>
              {hover.name}
            </div>
            <span style={{ opacity: 0.55 }}>{hover.code}</span>
          </div>
          {hover.hasData ? (
            <>
              <div style={{ marginTop: 8, display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
                <span style={{ opacity: 0.55, textTransform: "uppercase", letterSpacing: "0.08em" }}>
                  {mode === "passed" ? "became law" : mode === "pressure" ? "introduced" : "all activity"}
                </span>
                <span style={{ fontSize: 16, fontWeight: 600, color: colorForScore(hover.score).color }}>
                  {hover.score.toFixed(1)}
                </span>
              </div>
              <div style={{ marginTop: 6, height: 4, background: "rgba(255,255,255,0.12)" }}>
                <div style={{ height: "100%", width: `${hover.score}%`, background: colorForScore(hover.score).color }}></div>
              </div>
              <div style={{ marginTop: 8, opacity: 0.55, fontSize: 10 }}>
                CLICK → STATE DETAIL
              </div>
            </>
          ) : (
            <div style={{ marginTop: 8, opacity: 0.7, fontSize: 11 }}>
              No bills scored yet for this lens.
            </div>
          )}
        </div>
      )}
    </div>
  );
}

function MapLegend({ mode = "passed" }) {
  // Continuous gradient using the same 5 palette stops
  const gradient = `linear-gradient(to right, ${PALETTE_STOPS.map(s => `${s.c} ${s.at}%`).join(", ")})`;
  const modeLabel = mode === "passed" ? "Became law"
                   : mode === "pressure" ? "Pressure to change"
                   : "All activity";
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
      <div className="eyebrow">{modeLabel}</div>
      <div style={{ height: 10, background: gradient, borderRadius: 1 }}></div>
      <div style={{
        display: "flex", justifyContent: "space-between",
        fontFamily: "var(--mono)", fontSize: 10, color: "var(--ink-3)",
        letterSpacing: "0.06em"
      }}>
        <span>RESTRICTIVE</span>
        <span>NEUTRAL</span>
        <span>PROTECTIVE</span>
      </div>
      {/* No-data swatch */}
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 4,
        fontFamily: "var(--mono)", fontSize: 10, color: "var(--ink-3)", letterSpacing: "0.04em" }}>
        <svg width="22" height="10" style={{ display: "inline-block" }}>
          <defs>
            <pattern id="pl-nodata-swatch" patternUnits="userSpaceOnUse" width="6" height="6" patternTransform="rotate(45)">
              <rect width="6" height="6" fill="var(--bg-3)" />
              <line x1="0" y1="0" x2="0" y2="6" stroke="var(--ink-4)" strokeWidth="0.6" opacity="0.5" />
            </pattern>
          </defs>
          <rect width="22" height="10" fill="url(#pl-nodata-swatch)" />
        </svg>
        <span>NOT YET SCORED</span>
      </div>
    </div>
  );
}

window.PL_TileMap = Choropleth;     // legacy name kept for callers
window.PL_MapLegend = MapLegend;
}
