// dash-viz.jsx — live visualization engine + premium canvas variants + shared dashboard controls
const { useState: useV, useEffect: useVE, useRef: useVR } = React;

/* ---------- accent color (oklch -> sRGB so canvas works everywhere) ---------- */
function oklchToRgb(L, C, hDeg) {
  const h = hDeg * Math.PI / 180, a = C * Math.cos(h), b = C * Math.sin(h);
  let l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
  let m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
  let s = (L - 0.0894841775 * a - 1.2914855480 * b) ** 3;
  const r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
  const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
  const bb = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
  const f = x => x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(Math.max(0, x), 1 / 2.4) - 0.055;
  const c = x => Math.max(0, Math.min(255, Math.round(f(x) * 255)));
  return [c(r), c(g), c(bb)];
}
function accentRGB() {
  const h = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--accent-h")) || 140;
  return oklchToRgb(0.869, 0.275, h);
}
const rgba = (arr, a) => `rgba(${arr[0]},${arr[1]},${arr[2]},${a})`;

/* ---------- rAF driver: animates in focused browser, draws once otherwise ---------- */
function useRAF(active, cb) {
  const cbRef = useVR(cb); cbRef.current = cb;
  useVE(() => {
    let raf, last = performance.now(), alive = true;
    const tick = (now) => {
      if (!alive) return;
      const dt = Math.min(0.05, (now - last) / 1000); last = now;
      cbRef.current(dt, now / 1000);
      if (active) raf = requestAnimationFrame(tick);
    };
    cbRef.current(0, performance.now() / 1000); // immediate seed draw
    if (active) raf = requestAnimationFrame(tick);
    return () => { alive = false; cancelAnimationFrame(raf); };
  }, [active]);
}

/* ---------- canvas sizing (dpr-aware) ---------- */
function useCanvas(height) {
  const wrap = useVR(null), cv = useVR(null);
  const size = useVR({ w: 600, h: height });
  useVE(() => {
    const fit = () => {
      const el = wrap.current, c = cv.current; if (!el || !c) return;
      const dpr = Math.min(2, window.devicePixelRatio || 1);
      const w = el.clientWidth || 600, h = height;
      c.width = w * dpr; c.height = h * dpr; c.style.width = w + "px"; c.style.height = h + "px";
      const ctx = c.getContext("2d"); ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      size.current = { w, h };
    };
    fit();
    const ro = new ResizeObserver(fit); if (wrap.current) ro.observe(wrap.current);
    return () => ro.disconnect();
  }, [height]);
  return { wrap, cv, size };
}

function smoothPath(ctx, pts) {
  ctx.moveTo(pts[0][0], pts[0][1]);
  for (let i = 0; i < pts.length - 1; i++) {
    const [x0, y0] = pts[i], [x1, y1] = pts[i + 1];
    ctx.quadraticCurveTo(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
  }
  ctx.lineTo(pts[pts.length - 1][0], pts[pts.length - 1][1]);
}

function drawArea(ctx, w, h, data, max, ac, opts = {}) {
  ctx.clearRect(0, 0, w, h);
  // gridlines
  ctx.strokeStyle = "rgba(255,255,255,0.05)"; ctx.lineWidth = 1;
  for (let g = 1; g < 4; g++) { const y = (h * g) / 4; ctx.beginPath(); ctx.moveTo(0, y + 0.5); ctx.lineTo(w, y + 0.5); ctx.stroke(); }
  const n = data.length, step = w / (n - 1), pad = h * 0.12;
  const pts = data.map((v, i) => [i * step, h - pad - (Math.max(0, v) / max) * (h - pad * 1.4)]);
  // fill
  ctx.beginPath(); smoothPath(ctx, pts); ctx.lineTo(w, h); ctx.lineTo(0, h); ctx.closePath();
  const grad = ctx.createLinearGradient(0, 0, 0, h);
  grad.addColorStop(0, rgba(ac, opts.fill ?? 0.34)); grad.addColorStop(1, rgba(ac, 0));
  ctx.fillStyle = grad; ctx.fill();
  // line
  ctx.beginPath(); smoothPath(ctx, pts);
  ctx.strokeStyle = rgba(ac, 0.95); ctx.lineWidth = 2.4; ctx.lineJoin = "round"; ctx.lineCap = "round";
  ctx.shadowColor = rgba(ac, 0.55); ctx.shadowBlur = 14; ctx.stroke(); ctx.shadowBlur = 0;
  // leading dot
  const [lx, ly] = pts[pts.length - 1];
  ctx.beginPath(); ctx.arc(lx, ly, 4.2, 0, Math.PI * 2); ctx.fillStyle = "#fff"; ctx.fill();
  ctx.beginPath(); ctx.arc(lx, ly, 9, 0, Math.PI * 2); ctx.fillStyle = rgba(ac, 0.22); ctx.fill();
}

/* ================= VIEWER VIZ — concurrent viewers area chart ================= */
function ViewerViz({ running, target, onValue, height = 248 }) {
  const { wrap, cv, size } = useCanvas(height);
  const m = useVR(null), rep = useVR(0);
  if (!m.current) {
    const N = 72, data = [];
    for (let i = 0; i < N; i++) { const p = i / (N - 1); const ramp = 1 - Math.pow(1 - Math.min(1, p * 1.7), 2.3); data.push(target * ramp * (0.92 + 0.05 * Math.sin(i * 0.8))); }
    m.current = { data, cur: data[N - 1] };
  }
  useRAF(running, (dt, t) => {
    const mm = m.current, ctx = cv.current && cv.current.getContext("2d"); if (!ctx) return;
    const goal = running ? target : Math.min(mm.cur, target);
    if (dt > 0) {
      mm.cur += (goal - mm.cur) * Math.min(1, dt * 0.9);
      const noise = running && mm.cur > target * 0.35 ? (Math.sin(t * 1.6) + 0.5 * Math.sin(t * 0.7)) * target * 0.012 + (Math.random() - 0.5) * target * 0.012 : 0;
      const shown = Math.max(0, mm.cur + noise);
      mm.data.push(shown); if (mm.data.length > 72) mm.data.shift();
      if (now() - rep.current > 110) { rep.current = now(); onValue && onValue(Math.round(shown)); }
    }
    drawArea(ctx, size.current.w, size.current.h, m.current.data, target * 1.18, accentRGB());
  });
  useVE(() => { onValue && onValue(Math.round(m.current.cur)); }, []);
  return (
    <div ref={wrap} style={{ width: "100%" }}><canvas ref={cv} style={{ display: "block", width: "100%", height }} /></div>
  );
}
function now() { return performance.now(); }

/* ===== CONNECTION VIZ — stacked Connecting / Connected / Errors / Dead ===== */
const CONN_COLORS = {
  connected: () => accentRGB(),
  connecting: () => oklchToRgb(0.80, 0.135, 215),  // theme cyan-blue
  errors: () => oklchToRgb(0.83, 0.155, 82),       // warm amber
  dead: () => oklchToRgb(0.66, 0.20, 22),          // deep red
};
/* clean: Connected as a filled area (hero), other states as thin lines above */
function drawConnections(ctx, w, h, s, target) {
  ctx.clearRect(0, 0, w, h);
  ctx.strokeStyle = "rgba(255,255,255,0.05)"; ctx.lineWidth = 1;
  for (let g = 1; g < 4; g++) { const y = (h * g) / 4; ctx.beginPath(); ctx.moveTo(0, y + 0.5); ctx.lineTo(w, y + 0.5); ctx.stroke(); }
  const n = s.connected.length, step = w / (n - 1), ac = accentRGB();
  const smooth = (pts) => {
    ctx.moveTo(pts[0][0], pts[0][1]);
    for (let i = 0; i < pts.length - 1; i++) { const [x0, y0] = pts[i], [x1, y1] = pts[i + 1]; ctx.quadraticCurveTo(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2); }
    ctx.lineTo(pts[n - 1][0], pts[n - 1][1]);
  };
  // ---- single shared, auto-scaling Y axis ----
  // Every series is plotted against ONE max (the highest value across ALL lines), so a
  // line's on-screen height always reflects its real number: the bigger number sits higher,
  // and the whole chart rescales to whichever line is currently tallest.
  const padB = h * 0.08, padT = h * 0.12;
  let maxV = 1;
  // errors/dead excluded from the scale AND the drawn lines (customer view): they carry cumulative
  // lifetime counters that would dwarf the live connected/connecting values and flatten the real line.
  ["connected", "connecting"].forEach(k => { for (const v of s[k]) if (v > maxV) maxV = v; });
  maxV *= 1.15; // headroom so the tallest line doesn't clip the ceiling
  const Ym = v => h - padB - (Math.max(0, v) / maxV) * (h - padB - padT);
  const pm = s.connected.map((v, i) => [i * step, Ym(v)]);
  ctx.beginPath(); ctx.moveTo(0, h); ctx.lineTo(pm[0][0], pm[0][1]);
  for (let i = 0; i < pm.length - 1; i++) { const [x0, y0] = pm[i], [x1, y1] = pm[i + 1]; ctx.quadraticCurveTo(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2); }
  ctx.lineTo(pm[n - 1][0], pm[n - 1][1]); ctx.lineTo(w, h); ctx.closePath();
  const g1 = ctx.createLinearGradient(0, 0, 0, h);
  g1.addColorStop(0, rgba(ac, 0.42)); g1.addColorStop(1, rgba(ac, 0));
  ctx.fillStyle = g1; ctx.fill();
  ctx.beginPath(); smooth(pm);
  ctx.strokeStyle = rgba(ac, 0.98); ctx.lineWidth = 2.6; ctx.lineJoin = "round"; ctx.lineCap = "round";
  ctx.shadowColor = rgba(ac, 0.55); ctx.shadowBlur = 14; ctx.stroke(); ctx.shadowBlur = 0;
  const [lx, ly] = pm[n - 1];
  ctx.beginPath(); ctx.arc(lx, ly, 4, 0, Math.PI * 2); ctx.fillStyle = "#fff"; ctx.fill();
  ctx.beginPath(); ctx.arc(lx, ly, 9, 0, Math.PI * 2); ctx.fillStyle = rgba(ac, 0.22); ctx.fill();
  // ---- sub-state lines on the SAME axis (so heights are comparable to the hero line) ----
  const subs = [["connecting", CONN_COLORS.connecting()]];
  subs.forEach(([k, rgb]) => {
    const ps = s[k].map((v, i) => [i * step, Ym(v)]);
    ctx.beginPath(); smooth(ps);
    ctx.strokeStyle = rgba(rgb, 0.95); ctx.lineWidth = 2; ctx.lineJoin = "round"; ctx.lineCap = "round";
    ctx.shadowColor = rgba(rgb, 0.55); ctx.shadowBlur = 9; ctx.stroke(); ctx.shadowBlur = 0;
    const [dx, dy] = ps[n - 1];
    ctx.beginPath(); ctx.arc(dx, dy, 3, 0, Math.PI * 2); ctx.fillStyle = rgba(rgb, 1); ctx.fill();
  });
}

// Driven by the REAL polled stats ({connecting, connected, errors, dead}) — each line
// smoothly animates toward the live value (0 when idle), so the graph ramps up together
// with the stat tiles instead of jumping to a synthetic number. `target` only feeds the
// (now auto-scaling) draw routine.
function ConnectionViz({ running, stats, target, height = 248 }) {
  const { wrap, cv, size } = useCanvas(height);
  const m = useVR(null);
  if (!m.current) {
    const N = 72, zeros = () => Array.from({ length: N }, () => 0);
    m.current = { s: { connected: zeros(), connecting: zeros(), errors: zeros(), dead: zeros() }, cur: { connected: 0, connecting: 0, errors: 0, dead: 0 } };
  }
  useRAF(running, (dt) => {
    const mm = m.current, ctx = cv.current && cv.current.getContext("2d"); if (!ctx) return;
    const tgt = (running && stats) ? stats : { connected: 0, connecting: 0, errors: 0, dead: 0 };
    if (dt > 0) {
      const c = mm.cur, k = Math.min(1, dt * 0.8); // smooth approach toward the real value
      ["connected", "connecting", "errors", "dead"].forEach((key) => {
        c[key] += ((Number(tgt[key]) || 0) - c[key]) * k;
        mm.s[key].push(Math.max(0, c[key])); if (mm.s[key].length > 72) mm.s[key].shift();
      });
    }
    drawConnections(ctx, size.current.w, size.current.h, mm.s, target);
  });
  return (
    <div ref={wrap} style={{ width: "100%" }}><canvas ref={cv} style={{ display: "block", width: "100%", height }} /></div>
  );
}

/* ================= FOLLOW VIZ — cumulative followers climb ================= */
function FollowViz({ running, rate = 42, total0 = 12840, onTotal, height = 248 }) {
  const { wrap, cv, size } = useCanvas(height);
  const m = useVR(null), rep = useVR(0);
  if (!m.current) {
    const N = 64, data = []; let tot = total0 - rate * 18;
    for (let i = 0; i < N; i++) { tot += (rate / 60) * (18 / N) * 60 * (0.7 + 0.6 * Math.random()); data.push(tot); }
    m.current = { data, total: data[N - 1] };
  }
  useRAF(running, (dt, t) => {
    const mm = m.current, ctx = cv.current && cv.current.getContext("2d"); if (!ctx) return;
    if (dt > 0 && running) {
      mm.total += (rate / 60) * dt * 60 * (0.6 + 0.8 * Math.random());
      mm.data.push(mm.total); if (mm.data.length > 64) mm.data.shift();
      if (now() - rep.current > 140) { rep.current = now(); onTotal && onTotal(Math.round(mm.total)); }
    }
    const min = m.current.data[0], max = m.current.data[m.current.data.length - 1];
    const span = Math.max(1, max - min);
    const norm = m.current.data.map(v => v - min + span * 0.18);
    drawArea(ctx, size.current.w, size.current.h, norm, span * 1.3, accentRGB(), { fill: 0.3 });
  });
  useVE(() => { onTotal && onTotal(Math.round(m.current.total)); }, []);
  return (
    <div ref={wrap} style={{ width: "100%" }}><canvas ref={cv} style={{ display: "block", width: "100%", height }} /></div>
  );
}

/* ================= CHAT VIZ — live feed + msgs/min bars ================= */
const CHAT_USERS = ["xxNova", "kib_ttv", "ggwalrus", "p1xel", "mochi", "snypez", "deadlift", "vortex_", "lunaa", "froggy", "z3ro", "kashmir", "ttv_drips", "echoo", "binx"];
const CHAT_MSGS = ["W stream 🔥", "lets gooo", "first time here, ggs", "this dude cracked", "drop the settings", "KEKW", "+rep", "clip it!", "how many viewers rn", "insane gameplay", "poggers", "follow train?", "best on kick fr", "GG", "lets get 5k", "🔥🔥🔥", "actual cracked aim", "no way"];

function ChatViz({ running, rate = 180, height = 248 }) {
  const { wrap, cv, size } = useCanvas(96);
  const feedRef = useVR(null), m = useVR(null), acc = useVR(0), rep = useVR(0);
  const [feed, setFeed] = useV(() =>
    Array.from({ length: 9 }).map((_, i) => ({ id: i, u: CHAT_USERS[i % CHAT_USERS.length], t: CHAT_MSGS[i % CHAT_MSGS.length] })));
  if (!m.current) { m.current = { bars: Array.from({ length: 40 }, (_, i) => 0.3 + 0.5 * Math.abs(Math.sin(i * 0.6)) ), id: 1000 }; }
  useRAF(running, (dt, t) => {
    const mm = m.current, ctx = cv.current && cv.current.getContext("2d");
    if (dt > 0 && running) {
      acc.current += dt;
      const interval = 60 / rate;
      if (acc.current >= interval) {
        acc.current = 0;
        const msg = { id: mm.id++, u: CHAT_USERS[(Math.random() * CHAT_USERS.length) | 0], t: CHAT_MSGS[(Math.random() * CHAT_MSGS.length) | 0] };
        setFeed(f => [...f.slice(-13), msg]);
        mm.bars.push(0.6 + Math.random() * 0.4); if (mm.bars.length > 40) mm.bars.shift();
      } else { mm.bars[mm.bars.length - 1] = Math.max(mm.bars[mm.bars.length - 1], 0.2); }
    }
    if (ctx) {
      const { w, h } = size.current, ac = accentRGB(); ctx.clearRect(0, 0, w, h);
      const mid = h / 2;
      // center baseline
      ctx.strokeStyle = rgba(ac, 0.16); ctx.lineWidth = 1;
      ctx.beginPath(); ctx.moveTo(0, mid + 0.5); ctx.lineTo(w, mid + 0.5); ctx.stroke();
      const n = mm.bars.length, gap = 2.5, bw = w / n;
      mm.bars.forEach((v, i) => {
        const recency = i / (n - 1);            // 0 = oldest (left), 1 = newest (right)
        const on = running || i < n - 2;
        const half = Math.max(2, v * (h * 0.46));
        const x = i * bw + gap / 2, bwi = bw - gap, r = Math.min(2.5, bwi / 2);
        const a = on ? (0.4 + 0.5 * v) : 0.14;
        const g = ctx.createLinearGradient(0, mid - half, 0, mid + half);
        g.addColorStop(0, rgba(ac, a * 0.7));
        g.addColorStop(0.5, rgba(ac, Math.min(1, a + 0.3)));
        g.addColorStop(1, rgba(ac, a * 0.7));
        ctx.fillStyle = g;
        ctx.shadowColor = on ? rgba(ac, 0.6 * recency) : "transparent";
        ctx.shadowBlur = on ? 10 * recency : 0;
        ctx.beginPath(); ctx.roundRect(x, mid - half, bwi, half * 2, r); ctx.fill();
      });
      ctx.shadowBlur = 0;
      // soft leading glow dot on newest bar
      const lastV = mm.bars[mm.bars.length - 1];
      if (running) {
        const lx = w - bw / 2;
        ctx.beginPath(); ctx.arc(lx, mid - lastV * h * 0.46, 3, 0, Math.PI * 2);
        ctx.fillStyle = "#fff"; ctx.fill();
      }
    }
  });
  useVE(() => { const el = feedRef.current; if (el) el.scrollTop = el.scrollHeight; }, [feed]);
  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100%", minHeight: 320, gap: 12 }}>
      <div ref={feedRef} className="feed" style={{ flex: 1, minHeight: 0, overflowY: "auto", display: "flex", flexDirection: "column", gap: 9, paddingRight: 4 }}>
        {feed.map(msg => (
          <div key={msg.id} style={{ display: "flex", gap: 8, alignItems: "baseline", fontSize: 13.5, lineHeight: 1.35 }}>
            <span style={{ color: "var(--accent)", fontWeight: 600, flexShrink: 0 }}>{msg.u}</span>
            <span style={{ color: "var(--ink-2)" }}>{msg.t}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

/* ================= shared dashboard controls ================= */
function Toggle({ on, onChange }) {
  return <button className="tgl" data-on={on} onClick={() => onChange(!on)} aria-pressed={on} />;
}
function Seg({ value, options, onChange }) {
  return (
    <div className="seg">
      {options.map(o => <button key={o} data-on={value === o} onClick={() => onChange(o)}>{o}</button>)}
    </div>
  );
}
// Customers paste the whole profile URL into channel fields. "https://kick.com/name" is not a
// username, so the job runs against nothing, delivers zero, and they still get charged. Pass
// `channel` to a DField and it strips the link as they type AND tells them what we actually need -
// silently rewriting their input would fix the order but teach them nothing.
function normalizeKickChannel(raw) {
  const original = String(raw == null ? "" : raw);
  let v = original.trim();
  const looksLikeUrl = /^(https?:)?\/\//i.test(v) || /(^|\.)kick\.com/i.test(v) || v.indexOf("/") !== -1;
  if (looksLikeUrl) {
    v = v.replace(/^(https?:)?\/\//i, "");
    v = v.replace(/^(www\.)?kick\.com\/?/i, "");
    v = v.split(/[/?#]/)[0];
  }
  v = v.replace(/^@+/, "").trim();
  return { value: v, wasUrl: looksLikeUrl && v !== original.trim() };
}

const KVB_CHANNEL_HINT = {
  marginTop: 8,
  padding: "9px 12px",
  borderRadius: 9,
  background: "rgba(255, 190, 60, 0.10)",
  border: "1px solid rgba(255, 190, 60, 0.34)",
  color: "var(--ink-2)",
  fontSize: 12.5,
  lineHeight: 1.5,
};

function DField({ icon, value, onChange, placeholder, mono, channel }) {
  const [urlHint, setUrlHint] = useV(false);
  const handleChange = function (raw) {
    if (!channel) { onChange && onChange(raw); return; }
    const r = normalizeKickChannel(raw);
    if (r.wasUrl) { setUrlHint(true); }
    else if (!r.value) { setUrlHint(false); }
    onChange && onChange(r.value);
  };
  if (channel) {
    return (
      <div>
        <div className="dinput">
          {icon && <span style={{ color: "var(--ink-3)" }}><Icon name={icon} size={17} /></span>}
          <input className={mono ? "mono" : ""} value={value} placeholder={placeholder || "username"}
                 onChange={e => handleChange(e.target.value)} />
        </div>
        {urlHint && (
          <div style={KVB_CHANNEL_HINT}>
            <strong>We only need the username.</strong> We removed the kick.com link for you &mdash;
            for <span className="mono">kick.com/yourname</span> just enter <span className="mono">yourname</span>.
          </div>
        )}
      </div>
    );
  }
  return (
    <div className="dinput">
      {icon && <span style={{ color: "var(--ink-3)" }}><Icon name={icon} size={17} /></span>}
      <input className={mono ? "mono" : ""} value={value} placeholder={placeholder} onChange={e => onChange && onChange(e.target.value)} />
    </div>
  );
}
function PanelHead({ eyebrow, title, right }) {
  return (
    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 18, gap: 16 }}>
      <div>
        {eyebrow && <div className="eyebrow">{eyebrow}</div>}
        <h3 className="h2" style={{ fontSize: 22, marginTop: eyebrow ? 9 : 0 }}>{title}</h3>
      </div>
      {right}
    </div>
  );
}

Object.assign(window, { ViewerViz, ConnectionViz, FollowViz, ChatViz, Toggle, Seg, DField, PanelHead, accentRGB, normalizeKickChannel });
