/* global React, ReactDOM, DEALS */
const { useState, useEffect, useRef } = React;

function copyText(text, cb) {
  if (navigator.clipboard) navigator.clipboard.writeText(text).then(() => cb && cb());
  else cb && cb();
}

// Centralized in store.jsx — pull off window so admin and public agree.
const safeImgSrc = (window.XBStore && window.XBStore.safeImgSrc) || ((v) => v);

// Animated count-up
function useCountUp(target, duration = 1200, trigger = true) {
  const [val, setVal] = useState(0);
  useEffect(() => {
    if (!trigger) return;
    let raf;
    const start = performance.now();
    const tick = (now) => {
      const t = Math.min(1, (now - start) / duration);
      const eased = 1 - Math.pow(1 - t, 3);
      setVal(Math.round(target * eased));
      if (t < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [target, duration, trigger]);
  return val;
}

// Particles canvas
function Particles() {
  const ref = useRef(null);
  useEffect(() => {
    const canvas = ref.current;
    const ctx = canvas.getContext("2d");
    let w, h, raf;
    const dots = [];
    const resize = () => {
      w = canvas.width = window.innerWidth;
      h = canvas.height = window.innerHeight;
    };
    resize();
    window.addEventListener("resize", resize);
    for (let i = 0; i < 50; i++) {
      dots.push({
        x: Math.random() * w,
        y: Math.random() * h,
        vx: (Math.random() - 0.5) * 0.25,
        vy: -Math.random() * 0.4 - 0.1,
        r: Math.random() * 1.6 + 0.4,
        a: Math.random() * 0.5 + 0.1
      });
    }
    const loop = () => {
      const motion = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--motion") || 1);
      ctx.clearRect(0, 0, w, h);
      const accent = getComputedStyle(document.documentElement).getPropertyValue("--accent").trim() || "#22c55e";
      dots.forEach(d => {
        d.x += d.vx * motion;
        d.y += d.vy * motion;
        if (d.y < -10) { d.y = h + 10; d.x = Math.random() * w; }
        if (d.x < -10) d.x = w + 10;
        if (d.x > w + 10) d.x = -10;
        ctx.beginPath();
        ctx.fillStyle = accent;
        ctx.globalAlpha = d.a;
        ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
        ctx.fill();
      });
      ctx.globalAlpha = 1;
      raf = requestAnimationFrame(loop);
    };
    loop();
    return () => { window.removeEventListener("resize", resize); cancelAnimationFrame(raf); };
  }, []);
  return <canvas ref={ref} className="particles"></canvas>;
}

function CheckIcon() {
  return (
    <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
      <polyline points="3,8 7,12 13,4" />
    </svg>
  );
}

function Toast({ message, show }) {
  return (
    <div className={"toast" + (show ? " show" : "")}>
      <CheckIcon />{message}
    </div>
  );
}

function useToast() {
  const [state, setState] = useState({ msg: "", show: false });
  const t = useRef(null);
  return [state, (msg) => {
    if (t.current) clearTimeout(t.current);
    setState({ msg, show: true });
    t.current = setTimeout(() => setState(s => ({ ...s, show: false })), 2000);
  }];
}

// Twitch creator card — live status, viewers, follower count.
//
// Two data paths, in priority order:
//
// 1) Helix (official). Set `window.TWITCH_CONFIG = { clientId, accessToken, login }`
//    before this script loads. Use this when you have a backend proxy minting
//    App Access Tokens — never ship a client secret in browser code.
//
// 2) decapi.me (default). A public, CORS-enabled proxy that returns plain-text
//    Twitch stats without auth. Used by Twitch streamers for widgets.
//    Endpoints: /uptime/:login, /viewercount/:login, /followcount/:login.
//    Rate-limited and third-party, so polling stays conservative.

async function fetchTwitchHelix() {
  const cfg = window.TWITCH_CONFIG;
  if (!cfg || !cfg.clientId || !cfg.accessToken || !cfg.login) return null;
  try {
    const headers = { "Client-Id": cfg.clientId, "Authorization": "Bearer " + cfg.accessToken };
    const [userRes, streamRes] = await Promise.all([
      fetch(`https://api.twitch.tv/helix/users?login=${cfg.login}`, { headers }),
      fetch(`https://api.twitch.tv/helix/streams?user_login=${cfg.login}`, { headers })
    ]);
    const user = (await userRes.json()).data?.[0];
    const stream = (await streamRes.json()).data?.[0];
    let followers = null;
    if (user) {
      const fRes = await fetch(`https://api.twitch.tv/helix/channels/followers?broadcaster_id=${user.id}`, { headers });
      followers = (await fRes.json()).total;
    }
    return { isLive: !!stream, viewers: stream?.viewer_count ?? null, followers };
  } catch (e) { console.warn("Twitch Helix failed:", e); return null; }
}

async function fetchTwitchPublic(login) {
  if (!login) return null;
  const base = "https://decapi.me/twitch";
  try {
    const [uptimeRes, viewersRes, followersRes] = await Promise.all([
      fetch(`${base}/uptime/${login}`),
      fetch(`${base}/viewercount/${login}`),
      fetch(`${base}/followcount/${login}`)
    ]);
    // Bail on transport errors so the UI doesn't render decapi outage HTML
    // as a confident "OFFLINE" with null counts.
    if (!uptimeRes.ok || !viewersRes.ok || !followersRes.ok) return null;
    const [uptime, viewers, followers] = await Promise.all([
      uptimeRes.text(), viewersRes.text(), followersRes.text()
    ]);
    // decapi.me signals "offline" with an English sentence — both uptime and
    // viewercount return strings like "<login> is offline" when the channel
    // isn't streaming. Anything else (channel-not-found, API error) is also
    // an English sentence, so treat any non-clean-integer body as missing
    // rather than stripping digits and showing them as the count.
    const offline = /offline/i.test(uptime) || /offline/i.test(viewers);
    const cleanInt = (s) => /^\s*-?\d+\s*$/.test(s) ? parseInt(s, 10) : NaN;
    const viewerNum = cleanInt(viewers);
    const followerNum = cleanInt(followers);
    return {
      isLive: !offline,
      viewers: !offline && !isNaN(viewerNum) ? viewerNum : null,
      followers: !isNaN(followerNum) ? followerNum : null
    };
  } catch (e) { console.warn("Twitch public fetch failed:", e); return null; }
}

async function fetchTwitchData(login) {
  const helix = await fetchTwitchHelix();
  if (helix) return helix;
  return fetchTwitchPublic(login);
}

function formatCount(n) {
  if (n == null) return "—";
  if (typeof n === "string") return n;
  if (n >= 1000000) return (n / 1000000).toFixed(1) + "M";
  if (n >= 1000) return (n / 1000).toFixed(1) + "k";
  return String(n);
}

// Pull a Twitch handle out of a full URL like "https://twitch.tv/handle".
function twitchHandleFrom(url, fallback) {
  if (!url) return fallback;
  const m = String(url).match(/twitch\.tv\/([^/?#]+)/i);
  return m ? m[1] : fallback;
}

function parseFollowerCount(v, fallback) {
  if (typeof v === "number") return v;
  if (typeof v === "string") {
    const num = parseFloat(v);
    if (!isNaN(num)) {
      if (/m$/i.test(v)) return Math.round(num * 1000000);
      if (/k$/i.test(v)) return Math.round(num * 1000);
      return Math.round(num);
    }
  }
  return fallback;
}

function CreatorCard({ dealCount, profile }) {
  const p = profile || {};
  // Twitch login (the URL slug, used by the API) vs. display handle.
  // Default to the real channel `9xootic` when nothing is configured.
  const login = (window.TWITCH_CONFIG && window.TWITCH_CONFIG.login)
    || twitchHandleFrom(p.twitch, "9xootic");
  const handle = p.handle || login;
  const initialFollowers = parseFollowerCount(p.followers, null);

  const [tick, setTick] = useState(0);
  const [hasReal, setHasReal] = useState(false);
  // `loading` is true until the first fetch resolves, so we don't render a
  // confidently-wrong "OFFLINE" badge for the half-second before Twitch
  // data arrives. After the first response we keep showing the latest known
  // status even while subsequent polls are in flight.
  const [loading, setLoading] = useState(true);
  const [data, setData] = useState({
    isLive: false,
    viewers: null,
    followers: initialFollowers
  });

  // Keep follower fallback in sync with admin profile changes (until real
  // data lands).
  useEffect(() => {
    if (hasReal) return;
    setData(d => ({ ...d, followers: initialFollowers }));
  }, [initialFollowers, hasReal]);

  useEffect(() => {
    let cancelled = false;
    const refresh = async () => {
      const real = await fetchTwitchData(login);
      if (cancelled) return;
      if (real) {
        setHasReal(true);
        setData(real);
        setTick(t => t + 1);
      }
      setLoading(false);
    };
    refresh();
    // 45s keeps us comfortably under decapi.me rate limits while still
    // feeling live for viewer-count fluctuations.
    const id = setInterval(refresh, 45000);
    return () => { cancelled = true; clearInterval(id); };
  }, [login]);

  const { isLive, viewers, followers } = data;
  return (
    <div className="creator-card">
      <div className="cc-left">
        <div className="cc-avatar">
          <div className="cc-avatar-inner">
            <img src="assets/avatar.png" alt={p.name || "Creator"} />
          </div>
          {isLive && !loading && <span className="cc-live-ring"></span>}
        </div>
        <div className="cc-info">
          <div className="cc-name-row">
            <svg className="cc-twitch" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
              <path d="M3 3h18v12l-5 5h-4l-3 3H7v-3H3V3zm6 5h2v6H9V8zm5 0h2v6h-2V8z" />
            </svg>
            <span className="cc-handle">{handle}</span>
          </div>
          <div className={"cc-status " + (loading ? "offline" : (isLive ? "live" : "offline"))}>
            <span className="cc-status-dot"></span>
            {loading ? "CHECKING…" : (isLive ? "LIVE NOW" : "OFFLINE")}
          </div>
        </div>
      </div>
      <div className="cc-divider"></div>
      <div className="cc-stats">
        <div className="cc-stat">
          <span className="cc-stat-val" key={"f" + (followers == null ? "x" : followers)}>{formatCount(followers)}</span>
          <span className="cc-stat-lbl">followers</span>
        </div>
        <div className="cc-stat">
          <span className="cc-stat-val" key={"v" + tick}>{loading ? "…" : (viewers != null ? viewers.toLocaleString() : "—")}</span>
          <span className="cc-stat-lbl">{isLive ? "viewers now" : "viewers"}</span>
        </div>
        <div className="cc-stat">
          <span className="cc-stat-val accent">{dealCount}</span>
          <span className="cc-stat-lbl">active deals</span>
        </div>
      </div>
    </div>
  );
}

// Magnetic button
function MagneticButton({ children, className, onClick, href }) {
  const ref = useRef(null);
  const onMove = (e) => {
    const motion = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--motion") || 1);
    if (motion === 0) return;
    const r = ref.current.getBoundingClientRect();
    const x = (e.clientX - r.left - r.width / 2) * 0.15 * motion;
    const y = (e.clientY - r.top - r.height / 2) * 0.15 * motion;
    ref.current.style.transform = `translate(${x}px, ${y}px)`;
  };
  const onLeave = () => { if (ref.current) ref.current.style.transform = ""; };
  return (
    <a ref={ref} className={className} href={href || "#"} onMouseMove={onMove} onMouseLeave={onLeave} onClick={onClick}>
      {children}
    </a>
  );
}

// Top Pick
function TopPick({ deal, onClaim, onCopy }) {
  const [copied, setCopied] = useState(false);
  const [seen, setSeen] = useState(false);
  const ref = useRef(null);
  const bonusNum = useCountUp(deal.bonusValue, 1400, seen);
  const spinsNum = useCountUp(deal.spins, 1400, seen);

  useEffect(() => {
    const obs = new IntersectionObserver(([e]) => {
      if (e.isIntersecting) {
        setSeen(true);
        // Log a deal impression once per session (server-side dedup
        // handles duplicates if this fires for any reason).
        if (window.XBStore) window.XBStore.recordView(deal.id);
        obs.disconnect();
      }
    }, { threshold: 0.5 });
    if (ref.current) obs.observe(ref.current);
    return () => obs.disconnect();
  }, [deal.id]);

  const handleCopy = () => {
    copyText(deal.code, () => {
      setCopied(true);
      onCopy && onCopy(deal.code);
      // Fire-and-forget — rate-limited per session/code on the server.
      if (window.XBStore) window.XBStore.recordCopy(deal.id, deal.code);
      setTimeout(() => setCopied(false), 1600);
    });
  };

  return (
    <div ref={ref} className="toppick">
      <div className="toppick-shimmer"></div>
      <div className="toppick-confetti">
        {[...Array(8)].map((_, i) => <span key={i} style={{ "--i": i }}></span>)}
      </div>
      <div className="toppick-ribbon">
        <span className="ribbon-dot"></span>
        BEST DEAL · EDITOR'S #1
      </div>
      <div className="toppick-body">
        <div className="toppick-brand">
          <div className="toppick-logo" style={{ background: deal.tagBg, color: deal.tagColor }}>
            {safeImgSrc(deal.logo) ? <img src={safeImgSrc(deal.logo)} alt={deal.name} /> : deal.name.charAt(0)}
          </div>
          <div>
            <p className="toppick-eyebrow">{deal.name.toUpperCase()}</p>
            <p className="toppick-tagline">{deal.tagline}</p>
          </div>
        </div>
        <h2 className="toppick-bonus">
          <span className="num">€{bonusNum}</span>
          <span className="plus">+ {spinsNum} free spins</span>
        </h2>
        <div className="toppick-meta-row">
          <span>★ {deal.rating}/5</span>
          <span>·</span>
          <span>Min €{deal.minDeposit}</span>
          <span>·</span>
          <span>{deal.wager} wager</span>
          <span>·</span>
          <span className="accent">{deal.payout} payout</span>
        </div>
        <div className="toppick-action">
          <button className={"code-pill" + (copied ? " copied" : "")} onClick={handleCopy}>
            <span className="pill-label">CODE</span>
            <span className="pill-value">{deal.code}</span>
            <span className="pill-action">{copied ? "✓" : "COPY"}</span>
          </button>
          <MagneticButton className="btn-primary" onClick={(e) => { e.preventDefault(); onClaim(deal); }}>
            CLAIM NOW <span className="arrow">→</span>
          </MagneticButton>
        </div>
      </div>
    </div>
  );
}

// Mini deal row
function DealRow({ deal, onClaim, onCopy, idx }) {
  const [copied, setCopied] = useState(false);
  const [hovered, setHovered] = useState(false);
  const rowRef = useRef(null);

  // Impression tracking — fires once when the row scrolls 50% into view.
  useEffect(() => {
    if (!rowRef.current) return;
    const obs = new IntersectionObserver(([e]) => {
      if (e.isIntersecting) {
        if (window.XBStore) window.XBStore.recordView(deal.id);
        obs.disconnect();
      }
    }, { threshold: 0.5 });
    obs.observe(rowRef.current);
    return () => obs.disconnect();
  }, [deal.id]);
  const handleCopy = (e) => {
    e.stopPropagation();
    copyText(deal.code, () => {
      setCopied(true);
      onCopy && onCopy(deal.code);
      if (window.XBStore) window.XBStore.recordCopy(deal.id, deal.code);
      setTimeout(() => setCopied(false), 1500);
    });
  };
  return (
    <div
      ref={rowRef}
      className={"deal-row" + (hovered ? " hovered" : "")}
      style={{ animationDelay: (idx * 0.08) + "s" }}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      onClick={() => onClaim(deal)}
    >
      <div className="deal-row-logo" style={{ background: deal.tagBg, color: deal.tagColor }}>
        {safeImgSrc(deal.logo) ? <img src={safeImgSrc(deal.logo)} alt={deal.name} /> : deal.name.charAt(0)}
      </div>
      <div className="deal-row-main">
        <div className="deal-row-name">{deal.name}</div>
        <div className="deal-row-bonus">
          <strong>{deal.bonus}</strong> + {deal.spins} spins
        </div>
      </div>
      <button className={"deal-row-code" + (copied ? " copied" : "")} onClick={handleCopy}>
        <span className="lbl">{copied ? "✓ COPIED" : "TAP CODE"}</span>
        <span className="code">{deal.code}</span>
      </button>
      <div className="deal-row-cta">
        GET <span className="arrow">→</span>
      </div>
    </div>
  );
}

window.XB = { Particles, Toast, useToast, useCountUp, CreatorCard, MagneticButton, TopPick, DealRow, CheckIcon, copyText };
