// ============================================================
// components.jsx — Atlas map, radar, strain card, blend builder.
// ============================================================
(function () {
const { useState, useEffect, useRef, useMemo, useCallback } = React;
const {
  TERPENE_KEYS, TERPENE_META, TERPENE_P95, CATEGORY_COLORS,
  STRAINS, l2, cosine, projectPCA, findSimilar, findMatchesForBlend,
  projectBlendToPCA,
} = window.CannabisData;

// ── Tiny utility: clamp + lerp ────────────────────────────
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));

// ============================================================
// AtlasMap — PCA scatter with similarity edges, hover, select
// ============================================================
function AtlasMap({
  selected, onSelect, onHover, similarityThreshold, categoryFilter,
  colorMode, blendPoint,
}) {
  const wrapRef = useRef(null);
  const [size, setSize] = useState({ w: 800, h: 600 });
  const [hovered, setHovered] = useState(null);

  useEffect(() => {
    if (!wrapRef.current) return;
    const ro = new ResizeObserver(entries => {
      const r = entries[0].contentRect;
      setSize({ w: r.width, h: r.height });
    });
    ro.observe(wrapRef.current);
    return () => ro.disconnect();
  }, []);

  const pad = 56;
  const innerW = Math.max(50, size.w - pad * 2);
  const innerH = Math.max(50, size.h - pad * 2);

  const visible = useMemo(() =>
    STRAINS.filter(s => categoryFilter === 'All' || s.cat === categoryFilter),
    [categoryFilter]
  );

  const toScreen = useCallback((sx, sy) => ({
    x: pad + ((sx + 1) / 2) * innerW,
    y: pad + ((1 - sy) / 2) * innerH, // invert Y so + is up
  }), [innerW, innerH]);

  // Edges between visible strains above threshold
  const edges = useMemo(() => {
    const out = [];
    for (let i = 0; i < visible.length; i++) {
      for (let j = i + 1; j < visible.length; j++) {
        const sim = cosine(visible[i].vec, visible[j].vec);
        if (sim >= similarityThreshold) {
          out.push({ a: visible[i], b: visible[j], sim });
        }
      }
    }
    return out;
  }, [visible, similarityThreshold]);

  const sel = visible.find(s => s.slug === selected);
  const selSimSet = useMemo(() => {
    if (!sel) return new Set();
    return new Set(
      visible
        .filter(s => s.slug !== sel.slug)
        .filter(s => cosine(s.vec, sel.vec) >= similarityThreshold)
        .map(s => s.slug)
    );
  }, [sel, visible, similarityThreshold]);

  function nodeColor(s) {
    if (colorMode === 'category') return CATEGORY_COLORS[s.cat];
    if (colorMode === 'dom') return TERPENE_META[s.dom]?.color ?? '#888';
    if (colorMode === 'thc') {
      const t = clamp(((s.tot_thc ?? 18) - 16) / 8, 0, 1);
      // map low → mossy green, high → amber/red
      const hue = 110 - t * 80;
      return `oklch(0.72 0.14 ${hue})`;
    }
    return '#888';
  }
  function nodeR(s) { return 4 + (s.popularity ?? 0.5) * 10; }

  const blendPt = blendPoint ? toScreen(blendPoint[0], blendPoint[1]) : null;

  return (
    <div ref={wrapRef} style={{ position: 'absolute', inset: 0 }}>
      <svg
        width={size.w} height={size.h}
        style={{ position: 'absolute', inset: 0, fontFamily: '"JetBrains Mono", monospace' }}
      >
        <defs>
          <radialGradient id="atlas-bg" cx="50%" cy="45%" r="70%">
            <stop offset="0%" stopColor="rgba(64,86,46,0.22)" />
            <stop offset="60%" stopColor="rgba(20,30,18,0.05)" />
            <stop offset="100%" stopColor="rgba(13,16,10,0)" />
          </radialGradient>
        </defs>
        <rect width={size.w} height={size.h} fill="url(#atlas-bg)" />

        {/* axis grid */}
        <g stroke="rgba(232,228,213,0.06)" strokeWidth={0.5}>
          {[-1, -0.5, 0, 0.5, 1].map(t => {
            const p = toScreen(t, 0);
            return <line key={'v'+t} x1={p.x} y1={pad} x2={p.x} y2={size.h - pad} />;
          })}
          {[-1, -0.5, 0, 0.5, 1].map(t => {
            const p = toScreen(0, t);
            return <line key={'h'+t} x1={pad} y1={p.y} x2={size.w - pad} y2={p.y} />;
          })}
        </g>

        {/* axis labels */}
        <g fill="rgba(232,228,213,0.35)" fontSize={9} letterSpacing="0.16em">
          <text x={size.w - pad} y={size.h - pad + 22} textAnchor="end">
            PC1 → TERPINOLENE · PINENE  (SATIVA AXIS)
          </text>
          <g transform={`translate(${pad - 22}, ${pad}) rotate(-90)`}>
            <text x={0} y={0} textAnchor="end">
              PC2 → LINALOOL · NEROLIDOL  (INDICA AXIS)
            </text>
          </g>
          <text x={pad} y={size.h - pad + 22}>−1</text>
          <text x={size.w / 2} y={size.h - pad + 22} textAnchor="middle">0</text>
        </g>

        {/* Edges */}
        <g>
          {edges.map((e, i) => {
            const a = toScreen(e.a.pcaX, e.a.pcaY);
            const b = toScreen(e.b.pcaX, e.b.pcaY);
            const t = clamp((e.sim - similarityThreshold) / (1 - similarityThreshold), 0, 1);
            const isSelEdge = sel && (e.a.slug === sel.slug || e.b.slug === sel.slug);
            const op = isSelEdge ? 0.6 + t * 0.3 : 0.06 + t * 0.18;
            const col = isSelEdge ? '#bcd397' : '#7aa05a';
            return (
              <line key={i}
                x1={a.x} y1={a.y} x2={b.x} y2={b.y}
                stroke={col} strokeOpacity={op}
                strokeWidth={isSelEdge ? 1.2 + t * 0.8 : 0.5 + t * 0.5}
              />
            );
          })}
        </g>

        {/* Blend ghost point */}
        {blendPt && (
          <g>
            <circle cx={blendPt.x} cy={blendPt.y} r={18}
              fill="none" stroke="#f0c96a" strokeOpacity={0.25} strokeWidth={1} />
            <circle cx={blendPt.x} cy={blendPt.y} r={9}
              fill="none" stroke="#f0c96a" strokeOpacity={0.6} strokeWidth={1.2}
              strokeDasharray="3 2" />
            <circle cx={blendPt.x} cy={blendPt.y} r={3} fill="#f0c96a" />
            <text x={blendPt.x + 10} y={blendPt.y - 8}
              fontSize={9} fill="#f0c96a" letterSpacing="0.12em">YOUR BLEND</text>
          </g>
        )}

        {/* Nodes */}
        <g>
          {visible.map(s => {
            const p = toScreen(s.pcaX, s.pcaY);
            const r = nodeR(s);
            const c = nodeColor(s);
            const isSel = sel?.slug === s.slug;
            const isHov = hovered?.slug === s.slug;
            const isNeighbor = selSimSet.has(s.slug);
            const dim = sel && !isSel && !isNeighbor ? 0.32 : 1;
            return (
              <g key={s.slug}
                style={{ cursor: 'pointer', opacity: dim, transition: 'opacity 200ms' }}
                onMouseEnter={() => { setHovered(s); onHover?.(s); }}
                onMouseLeave={() => { setHovered(null); onHover?.(null); }}
                onClick={() => onSelect?.(s)}
              >
                {(isSel || isHov) && (
                  <circle cx={p.x} cy={p.y} r={r + 7}
                    fill="none" stroke={c} strokeOpacity={0.35} strokeWidth={1} />
                )}
                <circle cx={p.x} cy={p.y} r={r}
                  fill={c} fillOpacity={isSel ? 1 : 0.92}
                  stroke={isSel ? '#fff' : 'rgba(13,16,10,0.4)'}
                  strokeWidth={isSel ? 1.4 : 0.6}
                />
                {(isSel || isHov) && (
                  <g>
                    <text x={p.x} y={p.y - r - 8} textAnchor="middle"
                      fontFamily='"Newsreader", serif' fontSize={14}
                      fill="#f1ecd9" fontStyle="italic">
                      {s.name}
                    </text>
                    <text x={p.x} y={p.y + r + 14} textAnchor="middle"
                      fontSize={9} letterSpacing="0.14em"
                      fill="rgba(232,228,213,0.55)">
                      {s.cat.toUpperCase()} · {TERPENE_META[s.dom].short} · {s.tot_thc.toFixed(1)}% THC
                    </text>
                  </g>
                )}
              </g>
            );
          })}
        </g>
      </svg>
    </div>
  );
}

// ============================================================
// TerpeneRadar — pure SVG
// ============================================================
function TerpeneRadar({ strain, size = 280, compareStrain, blendVals }) {
  const cx = size / 2, cy = size / 2, r = size * 0.36;
  const n = TERPENE_KEYS.length;
  const angle = i => (i / n) * 2 * Math.PI - Math.PI / 2;
  const scaleVal = (k, v) => Math.min(1, (v ?? 0) / (TERPENE_P95[k] * 1.1));

  const pts = (src) =>
    TERPENE_KEYS.map((k, i) => {
      const v = src ? scaleVal(k, src[k]) : 0;
      const a = angle(i);
      return { x: cx + Math.cos(a) * r * v, y: cy + Math.sin(a) * r * v };
    });

  const strainPts = pts(strain);
  const cmpPts = compareStrain ? pts(compareStrain) : null;
  const blendPts = blendVals
    ? TERPENE_KEYS.map((k, i) => {
        const v = Math.min(1, (blendVals[i] ?? 0) / (TERPENE_P95[k] * 1.1));
        const a = angle(i);
        return { x: cx + Math.cos(a) * r * v, y: cy + Math.sin(a) * r * v };
      })
    : null;

  const toPath = (arr) =>
    arr.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(' ') + ' Z';

  const catColor = strain ? CATEGORY_COLORS[strain.cat] : '#bcd397';

  const labels = TERPENE_KEYS.map((k, i) => {
    const a = angle(i);
    const lr = r + 18;
    const v = strain?.[k] ?? 0;
    const isHi = v > TERPENE_P95[k] * 0.3;
    return {
      key: k,
      x: cx + Math.cos(a) * lr,
      y: cy + Math.sin(a) * lr,
      anchor: Math.cos(a) < -0.3 ? 'end' : Math.cos(a) > 0.3 ? 'start' : 'middle',
      val: v, isHi,
    };
  });

  return (
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ overflow: 'visible' }}>
      {[0.25, 0.5, 0.75, 1].map(f => (
        <polygon key={f}
          points={TERPENE_KEYS.map((_, i) => {
            const a = angle(i);
            return `${(cx + Math.cos(a) * r * f).toFixed(2)},${(cy + Math.sin(a) * r * f).toFixed(2)}`;
          }).join(' ')}
          fill="none"
          stroke={f === 1 ? 'rgba(232,228,213,0.18)' : 'rgba(232,228,213,0.07)'}
          strokeWidth={f === 1 ? 0.8 : 0.5}
        />
      ))}

      {TERPENE_KEYS.map((_, i) => {
        const a = angle(i);
        return <line key={i} x1={cx} y1={cy}
          x2={cx + Math.cos(a) * r} y2={cy + Math.sin(a) * r}
          stroke="rgba(232,228,213,0.08)" strokeWidth={0.5} />;
      })}

      {cmpPts && (
        <path d={toPath(cmpPts)} fill="rgba(240,201,106,0.10)"
          stroke="#f0c96a" strokeWidth={1} strokeDasharray="3 2" />
      )}

      {strain && (
        <path d={toPath(strainPts)} fill={catColor + '30'} stroke={catColor} strokeWidth={1.5} />
      )}

      {blendPts && (
        <path d={toPath(blendPts)} fill="rgba(240,201,106,0.18)"
          stroke="#f0c96a" strokeWidth={1.3} />
      )}

      {strain && strainPts.map((p, i) => {
        const k = TERPENE_KEYS[i];
        const v = strain[k] ?? 0;
        if (v <= 0.01) return null;
        return <circle key={i} cx={p.x} cy={p.y} r={2.5}
          fill={TERPENE_META[k].color} stroke={catColor} strokeWidth={0.5} />;
      })}

      {labels.map(lp => {
        const meta = TERPENE_META[lp.key];
        return (
          <g key={lp.key}>
            <text x={lp.x} y={lp.y - 1}
              textAnchor={lp.anchor} fontSize={9}
              fill={lp.isHi ? meta.color : 'rgba(232,228,213,0.42)'}
              fontFamily='"JetBrains Mono", monospace'
              letterSpacing="0.08em"
              fontWeight={lp.isHi ? 600 : 400}>
              {meta.short}
            </text>
            {lp.isHi && (
              <text x={lp.x} y={lp.y + 10}
                textAnchor={lp.anchor} fontSize={8}
                fontFamily='"JetBrains Mono", monospace'
                fill={meta.color + 'cc'}>
                {lp.val.toFixed(2)}
              </text>
            )}
          </g>
        );
      })}
    </svg>
  );
}

// ============================================================
// StrainCard — compact list entry
// ============================================================
function StrainCard({ strain, similarity, onClick, active }) {
  const meta = TERPENE_META[strain.dom];
  return (
    <button
      onClick={onClick}
      className="strain-card"
      data-active={active ? 'true' : 'false'}
    >
      <div className="row1">
        <span className="name">{strain.name}</span>
        <span className="cat" style={{
          color: CATEGORY_COLORS[strain.cat],
          borderColor: CATEGORY_COLORS[strain.cat] + '60',
          background: CATEGORY_COLORS[strain.cat] + '14',
        }}>{strain.cat}</span>
      </div>
      <div className="row2">
        <span className="dom" style={{ color: meta.color }}>
          <span className="dot" style={{ background: meta.color }}></span>
          {meta.label}
        </span>
        <span className="thc">THC {strain.tot_thc.toFixed(1)}%</span>
      </div>
      {similarity != null && (
        <div className="simbar">
          <div className="track">
            <div className="fill" style={{ width: `${clamp((similarity-0.6)/0.4, 0, 1)*100}%` }}></div>
          </div>
          <span className="pct">{(similarity*100).toFixed(0)}%</span>
        </div>
      )}
    </button>
  );
}

// ============================================================
// BlendBuilder — 11 sliders, live matches
// ============================================================
function BlendBuilder({ values, setValues, onSelect, selected }) {
  const totalRaw = values.reduce((s, v) => s + v, 0);
  const matches = useMemo(() => findMatchesForBlend(values, 6), [values]);

  const setOne = (idx, v) => {
    const next = values.slice();
    next[idx] = v;
    setValues(next);
  };

  function preset(name) {
    const presets = {
      'Sleep':        [0.7, 0.25, 0.15, 0.01, 0.05, 0.30, 0.05, 0.10, 0.08, 0.02, 0.10],
      'Focus':        [0.18, 0.22, 0.16, 0.62, 0.32, 0.04, 0.24, 0.08, 0.02, 0.06, 0.02],
      'Citrus mood':  [0.22, 0.26, 0.58, 0.05, 0.12, 0.06, 0.09, 0.10, 0.02, 0.07, 0.03],
      'Calm focus':   [0.34, 0.48, 0.30, 0.02, 0.10, 0.12, 0.08, 0.16, 0.04, 0.02, 0.05],
      'Clear':        [0.10, 0.20, 0.10, 0.10, 0.10, 0.05, 0.10, 0.05, 0.02, 0.04, 0.03],
    };
    if (presets[name]) setValues(presets[name]);
  }

  return (
    <div className="blend-grid">
      <div className="blend-controls">
        <div className="presets">
          {['Sleep', 'Focus', 'Citrus mood', 'Calm focus', 'Clear'].map(p =>
            <button key={p} onClick={() => preset(p)}>{p}</button>
          )}
        </div>
        <div className="sliders">
          {TERPENE_KEYS.map((k, i) => {
            const meta = TERPENE_META[k];
            const max = TERPENE_P95[k] * 1.4;
            return (
              <div key={k} className="slider-row">
                <div className="srow-head">
                  <span className="srow-name" style={{ color: meta.color }}>{meta.label}</span>
                  <span className="srow-val">{values[i].toFixed(2)}%</span>
                </div>
                <div className="srow-aroma">{meta.aroma}</div>
                <input
                  type="range" min="0" max={max} step="0.005"
                  value={values[i]}
                  onChange={e => setOne(i, parseFloat(e.target.value))}
                  style={{ '--c': meta.color }}
                />
              </div>
            );
          })}
        </div>
      </div>

      <div className="blend-side">
        <div className="blend-radar">
          <TerpeneRadar strain={null} blendVals={values} size={300} />
          <div className="blend-radar-caption">
            <span>YOUR BLEND</span>
            <span className="dim">total {totalRaw.toFixed(2)}%</span>
          </div>
        </div>

        <div className="match-header">
          <span className="kicker">NEAREST CULTIVARS</span>
          <span className="kicker dim">cosine ≥ threshold</span>
        </div>
        <div className="match-list">
          {matches.map(m => (
            <StrainCard key={m.strain.slug}
              strain={m.strain}
              similarity={m.sim}
              active={selected === m.strain.slug}
              onClick={() => onSelect?.(m.strain)} />
          ))}
        </div>
      </div>
    </div>
  );
}

window.PhytoUI = { AtlasMap, TerpeneRadar, StrainCard, BlendBuilder, clamp };
})();
