// ============= SHARED UI =============
const { useState, useEffect, useRef, useCallback, useContext, createContext } = React;

const AppCtx = createContext(null);
const useApp = () => useContext(AppCtx);

// accent presets (mirrors the app's color dock)
const ACCENTS = [
  { id:"blue",   v:"#2AABEE" },
  { id:"violet", v:"#7E72F2" },
  { id:"mint",   v:"#2BCB86" },
  { id:"amber",  v:"#F5A23D" },
  { id:"rose",   v:"#F2607A" },
];

// ---- robust in-view detection (scroll/rect based; IO-independent) ----
const _inviewSubs = new Set();
let _inviewBound = false;
function _runInview() { _inviewSubs.forEach((fn) => fn()); }
function subscribeInview(fn) {
  _inviewSubs.add(fn);
  if (!_inviewBound) {
    _inviewBound = true;
    window.addEventListener("scroll", _runInview, { passive: true });
    window.addEventListener("resize", _runInview, { passive: true });
  }
  fn();
  return () => _inviewSubs.delete(fn);
}
function useInView(ref, { threshold = 0.12, once = true } = {}) {
  const [seen, setSeen] = useState(false);
  const [armed, setArmed] = useState(false);
  useEffect(() => {
    const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduce) { setSeen(true); return; }
    setArmed(true);
    let done = false, raf = 0;
    const test = () => {
      const el = ref.current; if (!el) return false;
      const r = el.getBoundingClientRect();
      const vh = window.innerHeight || document.documentElement.clientHeight;
      if (r.height === 0 && r.width === 0) return false;
      return r.top < vh * (1 - threshold * 0.4) && r.bottom > vh * 0.06;
    };
    const reveal = () => { setSeen(true); done = true; };
    const onEvt = () => { if (once && done) return; if (test()) reveal(); else if (!once) setSeen(false); };
    const poll = () => { if (done) return; if (test()) { reveal(); return; } raf = requestAnimationFrame(poll); };
    raf = requestAnimationFrame(poll);
    const unsub = subscribeInview(onEvt);
    // safety: never let content stay hidden if rAF/scroll are starved
    const safety = setTimeout(() => { if (!done) reveal(); }, 2600);
    return () => { cancelAnimationFrame(raf); clearTimeout(safety); unsub(); };
  }, [ref, threshold, once]);
  return { seen, armed };
}

// scroll reveal
function Reveal({ children, className = "", delay = 0, as = "div", style = {}, ...rest }) {
  const ref = useRef(null);
  const { seen, armed } = useInView(ref, { threshold: 0.1 });
  const Tag = as;
  return (
    <Tag ref={ref} className={`reveal ${armed ? "armed" : ""} ${seen ? "in" : ""} ${className}`} style={{ transitionDelay: seen ? `${delay}ms` : "0ms", ...style }} {...rest}>
      {children}
    </Tag>
  );
}

// section header
function SecHead({ eyebrow, t1, t2, sub, center, id }) {
  return (
    <Reveal className={`sec-head ${center ? "sec-head--center" : ""}`} as="header">
      {eyebrow ? <span className="eyebrow">{eyebrow}</span> : null}
      <h2 className="sec-title" id={id}>{t1} {t2 ? <span className="dim2">{t2}</span> : null}</h2>
      {sub ? <p className="sec-sub">{sub}</p> : null}
    </Reveal>
  );
}

// count-up when visible
function useCountUp(target, dur = 1400) {
  const ref = useRef(null);
  const { seen } = useInView(ref, { threshold: 0.25 });
  const [val, setVal] = useState(0);
  useEffect(() => {
    if (!seen) return;
    let raf;
    const t0 = performance.now();
    const tick = (now) => {
      const p = Math.min(1, (now - t0) / dur);
      const eased = 1 - Math.pow(1 - p, 3);
      setVal(target * eased);
      if (p < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [seen, target, dur]);
  return [ref, val];
}

function fmtNum(n, dec = 0) {
  return n.toLocaleString("ru-RU", { minimumFractionDigits: dec, maximumFractionDigits: dec });
}

// generic stat with count-up (used in hero + features)
function CountStat({ value, unit, label, dec = 0, className = "" }) {
  // value may include suffix letters like "2.4М" — parse leading number
  const m = String(value).match(/^([\d.,\s]+)(.*)$/);
  const num = m ? parseFloat(m[1].replace(/\s/g, "").replace(",", ".")) : 0;
  const suffix = m ? m[2] : "";
  const [ref, v] = useCountUp(num, 1500);
  const hasDecimal = String(value).includes(".") || String(value).includes(",");
  const shown = hasDecimal ? v.toFixed(1) : fmtNum(Math.round(v));
  return (
    <div className={className} ref={ref}>
      <div className="hstat__v">{shown}{suffix}{unit ? <span className="u">{unit}</span> : null}</div>
      <div className="hstat__l">{label}</div>
    </div>
  );
}

// little inline sparkline path (deterministic from seed) — SMOOTH curve
function smoothPath(P) {
  if (!P || P.length < 2) return "";
  let d = `M ${P[0][0].toFixed(1)} ${P[0][1].toFixed(1)}`;
  for (let i = 0; i < P.length - 1; i++) {
    const p0 = P[i - 1] || P[i], p1 = P[i], p2 = P[i + 1], p3 = P[i + 2] || P[i + 1];
    const c1x = p1[0] + (p2[0] - p0[0]) / 6, c1y = p1[1] + (p2[1] - p0[1]) / 6;
    const c2x = p2[0] - (p3[0] - p1[0]) / 6, c2y = p2[1] - (p3[1] - p1[1]) / 6;
    d += ` C ${c1x.toFixed(1)} ${c1y.toFixed(1)}, ${c2x.toFixed(1)} ${c2y.toFixed(1)}, ${p2[0].toFixed(1)} ${p2[1].toFixed(1)}`;
  }
  return d;
}
function sparkPath(seed, w = 100, h = 24, pts = 12) {
  let s = seed;
  const rnd = () => { s = (s * 9301 + 49297) % 233280; return s / 233280; };
  const ys = Array.from({ length: pts }, (_, i) => 0.25 + 0.5 * rnd() + (i / pts) * 0.18);
  const max = Math.max(...ys), min = Math.min(...ys);
  const P = ys.map((y, i) => [i / (pts - 1) * w, h - (y - min) / (max - min || 1) * (h - 4) - 2]);
  return smoothPath(P);
}
// clean, lively, bounded wave inside a vertical band [top..bottom] — multiple gentle peaks, never overshoots
function wavePath(seed, w, h, top, bottom, pts = 9) {
  let s = seed * 2.3 + 7;
  const rnd = () => { s = (s * 9301 + 49297) % 233280; return s / 233280; };
  const mid = (top + bottom) / 2, amp = (bottom - top) / 2;
  const P = [];
  for (let i = 0; i < pts; i++) {
    const t = i / (pts - 1);
    let wob = 0.62 * Math.sin(t * Math.PI * 2.5 + seed) + 0.30 * Math.sin(t * Math.PI * 4.3 + seed * 1.7) + (rnd() - 0.5) * 0.42;
    wob = Math.max(-0.96, Math.min(0.96, wob));
    P.push([t * w, mid - amp * wob]);
  }
  return smoothPath(P);
}

Object.assign(window, { AppCtx, useApp, ACCENTS, Reveal, SecHead, useInView, useCountUp, fmtNum, CountStat, sparkPath, wavePath, smoothPath, useState, useEffect, useRef, useCallback });
