/* ============================================================
   radar-core.jsx — analytics + the radar plot + sparkline
   POLARITY-AWARE classifier: BULL-QQQ / BEAR-QQQ / WATCH / stable.
   Each asset is tagged by its natural relationship to QQQ
   (pro / counter / haven / cyclical_commod / mixed) and the
   onset is mapped to a QQQ implication accordingly.
   The underlying mechanic (COUPLE+/FLIP-) is still computed via
   mechanicOf() and surfaced in tooltips + method footer.
   Exposes: classify, mechanicOf, polarityOf, fmtB, fmtPct,
            signalMeta, polarityMeta, RadarPlot, Sparkline,
            SIG, POL, CAT_ORDER
   ============================================================ */
const { useState: useStateC, useMemo: useMemoC, useEffect: useEffectC, useRef: useRefC } = React;

/* -- signal vocabulary (QQQ-implication, not mechanic) ------ */
const SIG = {
  bull:   { key: 'bull',   label: 'BULL-QQQ', color: '#8FA67E', word: 'bullish for QQQ' },
  bear:   { key: 'bear',   label: 'BEAR-QQQ', color: '#D08E76', word: 'bearish for QQQ' },
  watch:  { key: 'watch',  label: 'watch',    color: '#DCB482', word: 'material move, evidence mixed' },
  stable: { key: 'stable', label: 'stable',   color: '#95A9AB', word: 'no actionable shift' },
};
const signalMeta = (k) => SIG[k] || SIG.stable;

/* -- polarity -- natural relationship to QQQ ---------------- */
const POL = {
  pro:             { key: 'pro',             label: 'pro-cyclical',     word: 'should move WITH QQQ',         weight: 1.0 },
  counter:         { key: 'counter',         label: 'counter-cyclical', word: 'should move AGAINST QQQ',      weight: 1.2 },
  haven:           { key: 'haven',           label: 'defensive haven',  word: 'safety bid when QQQ wobbles',  weight: 0.8 },
  cyclical_commod: { key: 'cyclical_commod', label: 'growth commodity', word: 'moves with global growth',     weight: 1.0 },
  mixed:           { key: 'mixed',           label: 'mixed / spread',   word: 'context-dependent',            weight: 0.5 },
};
const polarityMeta = (k) => POL[k] || POL.mixed;

/* default polarity by category */
const POLARITY_BY_CAT = {
  'Tech/Leadership':   'pro',
  'Mega-cap':          'pro',
  'Breadth/Factor':    'pro',
  'Index/Confirm':     'pro',
  'Futures':           'pro',
  'Crypto':            'pro',
  'Intl/Region':       'pro',
  'Sector':            'pro',
  'Credit/Financials': 'pro',
  'Income/REIT':       'haven',
  'Rates':             'counter',
  'Vol/Index':         'counter',
  'Commodity':         'cyclical_commod',
  'Spread':            'mixed',
};

/* per-symbol overrides -- highest-impact corrections to the
   category default. Edit this map as the universe evolves. */
const POLARITY_OVERRIDES = {
  /* commodity -> haven (safe-haven metals) */
  'GLD':'haven','SLV':'haven','GDX':'haven','SIL':'haven','IAU':'haven',
  /* commodity -> cyclical_commod (growth-sensitive) */
  'USO':'cyclical_commod','UNG':'cyclical_commod','OIH':'cyclical_commod','XOP':'cyclical_commod',
  'FCG':'cyclical_commod','DBC':'cyclical_commod','PDBC':'cyclical_commod','DBB':'cyclical_commod',
  'XME':'cyclical_commod','COPX':'cyclical_commod','CPER':'cyclical_commod','MOO':'cyclical_commod',
  'GUNR':'cyclical_commod','URA':'cyclical_commod','NLR':'cyclical_commod','LIT':'cyclical_commod',
  'REMX':'cyclical_commod','DBA':'cyclical_commod','AMLP':'cyclical_commod',
  /* credit -> pro (credit risk is risk-on) */
  'HYG':'pro','JBBB':'pro','EMHY':'pro','BKLN':'pro','EMB':'pro',
  /* credit -> counter (high-quality fixed income behaves like rates) */
  'LQD':'counter','JAAA':'counter','MBB':'counter','AGG':'counter','BND':'counter','BNDX':'counter',
  'BINC':'counter','TIP':'counter','VTIP':'counter',
  /* income / REIT / dividend -> haven */
  'XLU':'haven','VNQ':'haven','VNQI':'haven','REET':'haven','REM':'haven','XLRE':'haven',
  'SCHD':'haven','DVY':'haven','DIVO':'haven','JEPI':'haven','JEPQ':'haven','PFF':'haven',
  'QYLD':'haven','XYLD':'haven','QDVO':'haven','QQQI':'haven','SPHD':'haven',
  /* defensive sectors */
  'XLP':'haven','XLV':'haven',
  /* FX / dollar -> counter to QQQ */
  'UUP':'counter','DX-Y.NYB':'counter',
  /* rates / yields (Yahoo "^" symbols) */
  '^TNX':'counter','^FVX':'counter','^TYX':'counter','^IRX':'counter',
  /* vol indices */
  '^VIX':'counter','^VIX9D':'counter','^VIX3M':'counter','^VVIX':'counter','^MOVE':'counter','^SKEW':'counter',
  /* spreads */
  'HY_IG':'mixed','SMH_QQQ':'pro','VIX_TS':'counter','YC_10_5':'mixed',
};

function polarityOf(sym, cat) {
  if (POLARITY_OVERRIDES[sym]) return POLARITY_OVERRIDES[sym];
  return POLARITY_BY_CAT[cat] || 'mixed';
}

/* -- underlying mechanic (preserved for tooltips/method) ----- */
function mechanicOf(row, hl, thr) {
  const h = row.hl[hl];
  if (!h) return 'stable';
  const { now, ago5, onset } = h;
  const agree = row.agree;
  if (onset >= thr && now > 0.2 && now > ago5 && agree) return 'couple';
  if (onset <= -thr && now < -0.2 && !agree) return 'flip';
  if (Math.abs(onset) >= thr) return 'watch';
  return 'stable';
}
const MECH_LABEL = { couple: 'COUPLE+', flip: 'FLIP-', watch: 'watch', stable: 'stable' };

/* -- classification -- polarity-aware QQQ implication -------- */
function classify(row, hl, thr) {
  const h = row.hl[hl];
  if (!h) return 'stable';
  const { now, ago5, onset } = h;
  const agree = row.agree;
  const pol = polarityOf(row.sym, row.cat);

  if (pol === 'pro' || pol === 'cyclical_commod') {
    /* pro: BULL-QQQ = engaging UP with QQQ; BEAR-QQQ = decoupling down */
    if (onset >= thr && now > 0.2 && now > ago5 && agree) return 'bull';
    if (onset <= -thr && now < ago5 && !agree) return 'bear';
    if (Math.abs(onset) >= thr) return 'watch';
    return 'stable';
  }

  if (pol === 'counter') {
    /* counter: inverse to QQQ is the default state.
       BULL-QQQ when inverse DEEPENS (beta heading more negative).
       BEAR-QQQ when inverse BREAKS (beta rising toward zero or positive). */
    if (onset <= -thr && now <= -0.15) return 'bull';
    if (onset >= thr && now > -0.15) return 'bear';
    if (Math.abs(onset) >= thr) return 'watch';
    return 'stable';
  }

  if (pol === 'haven') {
    /* haven: beta typically near zero. Trajectory is the tell.
       BULL-QQQ when haven is EXITING (beta falling).
       BEAR-QQQ when haven is BID + coupling positively. */
    if (onset <= -thr) return 'bull';
    if (onset >= thr && now > 0.0) return 'bear';
    if (Math.abs(onset) >= thr) return 'watch';
    return 'stable';
  }

  /* mixed / spread / unknown polarity -- never bull/bear, only watch */
  if (Math.abs(onset) >= thr) return 'watch';
  return 'stable';
}

/* -- formatters --------------------------------------------- */
const fmtB = (v) => (v >= 0 ? '+' : '−') + Math.abs(v).toFixed(2);
const fmtPct = (v) => (v >= 0 ? '+' : '−') + Math.abs(v).toFixed(2) + '%';
const fmtOnset = (v) => (v >= 0 ? '+' : '−') + Math.abs(v).toFixed(2);

/* category display order for the radar arcs (others fall after) */
const CAT_ORDER = [
  'Tech/Leadership', 'Breadth/Factor', 'Index/Confirm', 'Intl/Region',
  'Commodity', 'Crypto', 'Credit/Financials', 'Income/REIT', 'Rates',
  'Mega-cap', 'Vol/Index', 'Futures', 'Spread', 'Sector',
];
const catRank = (c) => { const i = CAT_ORDER.indexOf(c); return i < 0 ? 99 : i; };

/* ============================================================
   SPARKLINE -- 25-pt beta trend (the signal to trust)
   ============================================================ */
// monotone cubic Hermite (Fritsch-Carlson) -- smooth, no overshoot past data points
function monotonePath(xs, ys) {
  const n = xs.length;
  if (n < 2) return '';
  if (n === 2) return `M ${xs[0].toFixed(2)} ${ys[0].toFixed(2)} L ${xs[1].toFixed(2)} ${ys[1].toFixed(2)}`;
  const dx = [], dy = [], m = [];
  for (let i = 0; i < n - 1; i++) { dx[i] = xs[i + 1] - xs[i]; dy[i] = ys[i + 1] - ys[i]; m[i] = dy[i] / dx[i]; }
  const t = [m[0]];
  for (let i = 1; i < n - 1; i++) t[i] = m[i - 1] * m[i] <= 0 ? 0 : (m[i - 1] + m[i]) / 2;
  t[n - 1] = m[n - 2];
  for (let i = 0; i < n - 1; i++) {
    if (m[i] === 0) { t[i] = 0; t[i + 1] = 0; }
    else {
      const a = t[i] / m[i], b = t[i + 1] / m[i], s = a * a + b * b;
      if (s > 9) { const tau = 3 / Math.sqrt(s); t[i] = tau * a * m[i]; t[i + 1] = tau * b * m[i]; }
    }
  }
  let d = `M ${xs[0].toFixed(2)} ${ys[0].toFixed(2)}`;
  for (let i = 0; i < n - 1; i++) {
    const h = dx[i];
    d += ` C ${(xs[i] + h / 3).toFixed(2)} ${(ys[i] + t[i] * h / 3).toFixed(2)}, ${(xs[i + 1] - h / 3).toFixed(2)} ${(ys[i + 1] - t[i + 1] * h / 3).toFixed(2)}, ${xs[i + 1].toFixed(2)} ${ys[i + 1].toFixed(2)}`;
  }
  return d;
}

let __sparkSeq = 0;
function Sparkline({ series, color, height = 38, showZero = true, showDot = true, strokeW = 2 }) {
  if (!series || !series.length) return null;
  const uid = useMemoC(() => 'spk' + (++__sparkSeq), []);
  const W = 100, H = height, pad = 3;
  const lo = Math.min(...series, 0), hi = Math.max(...series, 0);
  const span = (hi - lo) || 1;
  const x = (i) => pad + (i / (series.length - 1)) * (W - 2 * pad);
  const y = (v) => pad + (1 - (v - lo) / span) * (H - 2 * pad);
  const xs = series.map((_, i) => x(i)), ys = series.map((v) => y(v));
  const line = monotonePath(xs, ys);
  const baseY = H - pad;
  const area = `${line} L ${xs[xs.length - 1].toFixed(2)} ${baseY.toFixed(2)} L ${xs[0].toFixed(2)} ${baseY.toFixed(2)} Z`;
  const zeroY = y(0);
  const last = series[series.length - 1];
  return (
    <svg className="spark-svg" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" style={{ height }}>
      <defs>
        <linearGradient id={uid} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor={color} stopOpacity="0.20" />
          <stop offset="100%" stopColor={color} stopOpacity="0" />
        </linearGradient>
      </defs>
      <path className="spark-area" d={area} fill={`url(#${uid})`} />
      {showZero && lo < 0 && hi > 0 && (
        <line className="spark-zero" x1={pad} y1={zeroY} x2={W - pad} y2={zeroY} vectorEffect="non-scaling-stroke" />
      )}
      <path className="spark-path" d={line} stroke={color} strokeWidth={strokeW} vectorEffect="non-scaling-stroke" />
      {showDot && <circle className="spark-dot" cx={x(series.length - 1)} cy={y(last)} r={2.4} fill={color} vectorEffect="non-scaling-stroke" />}
    </svg>
  );
}

/* ============================================================
   RADAR PLOT -- active instruments on category-grouped spokes.
   radius = |beta to QQQ|, faint dot = beta 5d ago -> bold dot = beta now
   (the drift = the coupling onset). color = signal. halo = alert.
   ============================================================ */
const VB = 1000, CX = 500, CY = 500, MAXR = 320;
const LABELR = MAXR + 18, ARCR = MAXR + 44, SECR = MAXR + 88;
const CORER = 62, INNERR = 74;

function RadarPlot({ rows, hl, thr, betaCap }) {
  const [hover, setHover] = useStateC(null);
  const [animKey, setAnimKey] = useStateC(0);
  const [entered, setEntered] = useStateC(true);
  useEffectC(() => {
    if (animKey === 0) return;
    setEntered(false);
    const t = setTimeout(() => setEntered(true), 60);
    return () => clearTimeout(t);
  }, [animKey]);

  const cap = betaCap || 2.5;
  const rOf = (b) => INNERR + Math.min(1, Math.abs(b) / cap) * (MAXR - INNERR);
  const angOf = (i, n) => (-90 + (i * 360) / n) * Math.PI / 180;

  const nodes = useMemoC(() => {
    const enriched = rows.map((r) => {
      const h = r.hl[hl];
      return { ...r, _now: h.now, _ago: h.ago5, _onset: h.onset, _series: h.series,
               _sig: classify(r, hl, thr), _mech: mechanicOf(r, hl, thr), _pol: polarityOf(r.sym, r.cat) };
    });
    enriched.sort((a, b) => (catRank(a.cat) - catRank(b.cat)) || (Math.abs(b._onset) - Math.abs(a._onset)) || a.sym.localeCompare(b.sym));
    return enriched.map((d, i) => ({ ...d, i }));
  }, [rows, hl, thr]);

  const n = nodes.length;
  const secRanges = useMemoC(() => {
    const cats = [];
    nodes.forEach((d) => { if (!cats.find((c) => c.key === d.cat)) cats.push({ key: d.cat, idxs: [] }); cats.find((c) => c.key === d.cat).idxs.push(d.i); });
    const pad = n > 1 ? (360 / n) * Math.PI / 180 * 0.4 : 0;
    return cats.map((c) => {
      const first = Math.min(...c.idxs), last = Math.max(...c.idxs);
      return { ...c, a0: angOf(first, n) - pad, a1: angOf(last, n) + pad, mid: angOf((first + last) / 2, n) };
    });
  }, [nodes, n]);

  if (!n) return null;
  const rings = [0.25, 0.5, 0.75, 1.0];
  const arc = (r, a0, a1) => {
    const x0 = CX + r * Math.cos(a0), y0 = CY + r * Math.sin(a0);
    const x1 = CX + r * Math.cos(a1), y1 = CY + r * Math.sin(a1);
    const large = (a1 - a0) > Math.PI ? 1 : 0;
    return `M ${x0.toFixed(1)} ${y0.toFixed(1)} A ${r} ${r} 0 ${large} 1 ${x1.toFixed(1)} ${y1.toFixed(1)}`;
  };
  const ptAt = (i, b) => { const a = angOf(i, n), r = rOf(b); return [CX + r * Math.cos(a), CY + r * Math.sin(a)]; };

  return (
    <div className="radar-stage">
      <svg className="radar-svg" viewBox={`0 0 ${VB} ${VB}`} role="img" aria-label="QQQ-implication radar of active instruments">
        <circle className="rr-ring zero" cx={CX} cy={CY} r={INNERR} />
        {rings.map((rv) => <circle key={rv} className="rr-ring" cx={CX} cy={CY} r={rOf(rv * cap)} />)}
        <text className="rr-ring-lab" x={CX + 5} y={CY - INNERR - 3}>{'β'} 0</text>
        {rings.map((rv) => <text key={'l' + rv} className="rr-ring-lab" x={CX + 5} y={CY - rOf(rv * cap) - 3}>{(rv * cap).toFixed(1)}</text>)}

        {nodes.map((d) => {
          const a = angOf(d.i, n);
          const sx = CX + INNERR * Math.cos(a), sy = CY + INNERR * Math.sin(a);
          const ex = CX + MAXR * Math.cos(a), ey = CY + MAXR * Math.sin(a);
          return <line key={'sp' + d.sym} className="rr-spoke" x1={sx} y1={sy} x2={ex} y2={ey} />;
        })}

        {secRanges.map((s) => {
          const [sx, sy] = [CX + SECR * Math.cos(s.mid), CY + SECR * Math.sin(s.mid)];
          const anchor = Math.cos(s.mid) > 0.1 ? 'start' : Math.cos(s.mid) < -0.1 ? 'end' : 'middle';
          const short = s.key.split('/')[0];
          return (
            <g key={s.key}>
              <path className="rr-arc" d={arc(ARCR, s.a0, s.a1)} stroke="#95A9AB" />
              <text className="rr-sec" x={sx} y={sy} textAnchor={anchor} dominantBaseline="middle">{short}</text>
            </g>
          );
        })}

        {nodes.map((d) => {
          const a = angOf(d.i, n);
          const lx = CX + LABELR * Math.cos(a), ly = CY + LABELR * Math.sin(a);
          const anchor = Math.cos(a) > 0.1 ? 'start' : Math.cos(a) < -0.1 ? 'end' : 'middle';
          const dim = hover != null && hover !== d.i;
          return <text key={'tk' + d.sym} className={`rr-tk${dim ? ' dim' : ''}`} x={lx} y={ly} textAnchor={anchor} dominantBaseline="middle"
            onMouseEnter={() => setHover(d.i)} onMouseLeave={() => setHover(null)}>{d.sym}</text>;
        })}

        <g key={animKey}>
          {nodes.map((d) => {
            const [ax, ay] = ptAt(d.i, d._ago);
            const [nx, ny] = ptAt(d.i, d._now);
            const col = signalMeta(d._sig).color;
            return <line key={'dr' + d.sym} className="rr-drift" x1={ax} y1={ay} x2={nx} y2={ny}
              stroke={col} strokeWidth={d._sig === 'stable' ? 1.4 : 2.4}
              style={{ opacity: entered ? (d._sig === 'stable' ? 0.4 : 0.85) : 0, transitionDelay: `${0.2 + d.i * 0.01}s` }} />;
          })}
          {nodes.map((d) => {
            const [ax, ay] = ptAt(d.i, d._ago);
            return <circle key={'ag' + d.sym} className="rr-ago" cx={ax} cy={ay} r={3} fill="none"
              stroke={signalMeta(d._sig).color} strokeWidth={1.4}
              style={{ opacity: entered ? 0.5 : 0, transitionDelay: `${0.3 + d.i * 0.01}s` }} />;
          })}
          {nodes.map((d) => {
            const [nx, ny] = ptAt(d.i, d._now);
            const col = signalMeta(d._sig).color;
            const alert = d._sig === 'bull' || d._sig === 'bear';
            return (
              <g key={'nw' + d.sym}>
                {alert && entered && <circle className="rr-halo" cx={nx} cy={ny} r={13} stroke={col} />}
                <circle className="rr-now" cx={nx} cy={ny} r={alert ? 7 : 5} fill={col} stroke="#fff" strokeWidth={1.8}
                  style={{ opacity: entered ? 1 : 0, transform: entered ? 'scale(1)' : 'scale(0)', transitionDelay: `${0.55 + d.i * 0.012}s` }}
                  onMouseEnter={() => setHover(d.i)} onMouseLeave={() => setHover(null)} />
              </g>
            );
          })}
          <circle className="rr-core" cx={CX} cy={CY} r={CORER} />
          <text className="rr-core-t" x={CX} y={CY + 1} textAnchor="middle" dominantBaseline="middle">QQQ</text>
          <text className="rr-core-s" x={CX} y={CY + 24} textAnchor="middle">BENCHMARK</text>
        </g>
      </svg>

      {hover != null && nodes[hover] && (() => {
        const d = nodes[hover];
        const [hx, hy] = ptAt(d.i, d._now);
        const m = signalMeta(d._sig);
        const pm = polarityMeta(d._pol);
        return (
          <div className="radar-tip" style={{ left: (hx / VB) * 100 + '%', top: (hy / VB) * 100 + '%' }}>
            <div className="rt-tk">{d.sym}</div>
            <div className="rt-nm">{window.tickerName(d.sym, d.name)} {'·'} {d.cat}</div>
            <div className="rt-row"><span className="lab">polarity</span><span className="val" style={{ opacity: 0.85 }}>{pm.label}</span></div>
            <div className="rt-row"><span className="lab">{'β'} now</span><span className="val">{fmtB(d._now)}</span></div>
            <div className="rt-row"><span className="lab">{'β'} 5d ago</span><span className="val">{fmtB(d._ago)}</span></div>
            <div className="rt-row"><span className="lab">onset {'Δ'}</span><span className="val" style={{ color: d._onset >= 0 ? '#B8CDA0' : '#E7B8A8' }}>{fmtOnset(d._onset)}</span></div>
            <div className="rt-row"><span className="lab">5d move</span><span className="val">{fmtPct(d.amove)} <span style={{ opacity: 0.5 }}>vs {fmtPct(d.qmove)}</span></span></div>
            <div className="rt-row"><span className="lab">mechanic</span><span className="val" style={{ opacity: 0.7, fontSize: 10 }}>{MECH_LABEL[d._mech]}</span></div>
            <div className={`rt-tag ${m.key}`}>{'◎'} {m.label} {'·'} {m.word}</div>
          </div>
        );
      })()}

      <div className="radar-legend">
        <div className="rleg-g">
          <div className="rleg-t">Radius = |{'β'}| to QQQ</div>
          <div style={{ width: 130 }}>
            <span className="rleg-bar" />
            <div className="rleg-scale-labs"><span>0 {'·'} at rim</span><span>{cap.toFixed(1)}+</span></div>
          </div>
        </div>
        <div className="rleg-g">
          <div className="rleg-t">Color = QQQ implication</div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
            <div className="rleg-row"><span className="sw" style={{ background: SIG.bull.color }} /> BULL-QQQ {'·'} bullish for QQQ</div>
            <div className="rleg-row"><span className="sw" style={{ background: SIG.bear.color }} /> BEAR-QQQ {'·'} bearish for QQQ</div>
            <div className="rleg-row"><span className="sw" style={{ background: SIG.watch.color }} /> watch {'·'} evidence mixed</div>
          </div>
        </div>
        <div className="rleg-g">
          <div className="rleg-t">Drift = the onset</div>
          <div className="rleg-row"><span className="rleg-drift"><span style={{ width: 7, height: 7, border: '1.4px solid #95A9AB', borderRadius: '50%', display: 'inline-block' }} /><span style={{ color: '#95A9AB' }}>{'→'}</span><span style={{ width: 9, height: 9, background: '#8FA67E', borderRadius: '50%', display: 'inline-block' }} /></span> {'β'} 5d ago {'→'} {'β'} now</div>
          <div className="rleg-row" style={{ fontSize: 10.5, color: 'var(--stone-1)' }}>read against polarity: pro outward = bullish, counter outward = bearish</div>
        </div>
        <button className="rad-replay" onClick={() => { setEntered(false); setAnimKey((k) => k + 1); }} style={{ alignSelf: 'center' }}>{'↻'} Replay</button>
      </div>
    </div>
  );
}

Object.assign(window, { classify, mechanicOf, polarityOf, fmtB, fmtPct, fmtOnset, signalMeta, polarityMeta, SIG, POL, CAT_ORDER, catRank, Sparkline, RadarPlot });
