(function () {
  'use strict';

  const $ = (s) => document.querySelector(s);

  const wheelSvg     = $('#wheelSvg');
  const wheelImages  = $('#wheelImages');
  const shopGrid     = $('#shopGrid');
  const shopPager    = $('#shopPager');
  const shopFilter   = $('#shopFilter');
  // v-shop: state paginasi + filter toko
  let shopAllItems   = [];          // semua item dari API/initial
  let shopSort       = 'default';   // 'default' | 'food' | 'cheap' | 'expensive' | 'zonk'
  let shopPage       = 1;
  const SHOP_PER_PAGE = 20;
  const winnersEl    = $('#winnersList');
  const spinBtn      = $('#spinBtn');
  const spinBtnSub   = $('#spinBtnSub');
  const winModal     = $('#winModal');
  const winImg       = $('#winImg');
  const winName      = $('#winName');
  const winBadge     = $('#winBadge');
  const winSub       = $('#winSub');
  const winClose     = $('#winClose');
  const winClaim     = $('#winClaim');

  // Global (all pages)
  const totalEl      = $('#totalPrizes');
  const heroSubEl    = $('#heroSub');
  const navAuth      = $('#navAuth');
  const loginModal   = $('#loginModal');
  const loginForm    = $('#loginForm');
  const loginUser    = $('#loginUser');
  const loginPass    = $('#loginPass');
  const loginErrEl   = $('#loginError');
  const loginCancel  = $('#loginCancel');
  const loginSubmit  = $('#loginSubmit');
  const toastEl      = $('#toast');
  const toastIcon    = $('#toastIcon');
  const toastTitle   = $('#toastTitle');
  const toastDesc    = $('#toastDesc');
  const floatSlot    = $('#floatingSlot');
  const navToggle    = $('#navToggle');
  const navMenu      = $('#navMenu');
  const caraModal    = $('#caraModal');
  const faqModal     = $('#faqModal');
  const openCaraBtn  = $('#openCaraBtn');
  const openFaqBtn   = $('#openFaqBtn');
  const closeCaraBtn = $('#closeCaraBtn');
  const closeFaqBtn  = $('#closeFaqBtn');
  const confirmModal = $('#confirmRedeemModal');
  const confirmImg   = $('#confirmItemImg');
  const confirmName  = $('#confirmItemName');
  const confirmPrice = $('#confirmItemPrice');
  const confirmOk    = $('#confirmRedeemOk');
  const confirmCancel= $('#confirmRedeemCancel');

  // ---------- State ----------
  let prizes = [];
  let me = null;
  let isSpinning = false;
  let suppressBalanceUI = false;
  let balanceRevealTimer = null;
  let currentRotation = 0;
  let lastWheelHash  = '';
  let lastWinnersHash = '';
  let lastShopHash = '';
  let lastMeHash = '';
  let pollTimer = null;
  let lastFloatingHTML = '';
  let pendingRedeemItem = null;        // { id, name, price, zonk, type, image, btnElement }
  const knownWinnerIds = new Set();    // for sliding-window flash

  const HAS_WHEEL = !!spinBtn;         // "full" mode if spin button exists (index page)

  const fmt = n => new Intl.NumberFormat('id-ID').format(n);
  function hash(s) { let h = 0; for (let i = 0; i < s.length; i++) { h = ((h << 5) - h + s.charCodeAt(i)) | 0; } return h; }
  function escapeHtml(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }

  const SOUND_WIN  = '/assets/winsound.mp3';
  const SOUND_LOSE = '/assets/losesound.mp3';
  let winAudio = null, loseAudio = null, audioUnlocked = false;

  function ensureAudio() {
    if (winAudio && loseAudio) return;
    try {
      winAudio  = new Audio(SOUND_WIN);
      loseAudio = new Audio(SOUND_LOSE);
      winAudio.preload  = 'auto';
      loseAudio.preload = 'auto';
    } catch (e) { /* browser tanpa dukungan Audio → abaikan, fungsi lain tetap jalan */ }
  }

  function unlockAudio() {
    ensureAudio();
    if (audioUnlocked || !winAudio) return;
    [winAudio, loseAudio].forEach(a => {
      if (!a) return;
      try {
        a.muted = true;
        const p = a.play();
        if (p && typeof p.then === 'function') {
          p.then(() => { try { a.pause(); a.currentTime = 0; a.muted = false; } catch (_) {} })
           .catch(() => { try { a.muted = false; } catch (_) {} });
        } else {
          a.pause(); a.currentTime = 0; a.muted = false;
        }
      } catch (e) { try { a.muted = false; } catch (_) {} }
    });
    audioUnlocked = true;
  }

  function playSpinSound(type) {
    ensureAudio();
    if (!winAudio || !loseAudio) return;
    try { winAudio.pause();  winAudio.currentTime  = 0; } catch (e) {}
    try { loseAudio.pause(); loseAudio.currentTime = 0; } catch (e) {}
    const a = (type === 'zonk') ? loseAudio : winAudio;
    try {
      a.currentTime = 0;
      const p = a.play();
      if (p && typeof p.catch === 'function') p.catch(() => {});
    } catch (e) { /* abaikan kegagalan play */ }
  }

  function showToast(title, desc, type) {
    if (!toastEl) return;
    toastEl.className = 'toast ' + (type || '');
    if (toastIcon) {
      toastIcon.innerHTML = type === 'success' ?
        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>' :
        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12" y2="16.01"/></svg>';
    }
    if (toastTitle) toastTitle.textContent = title || '';
    if (toastDesc)  toastDesc.textContent  = desc  || '';
    toastEl.classList.add('show');
    clearTimeout(toastEl._t);
    toastEl._t = setTimeout(() => toastEl.classList.remove('show'), 3800);
  }

  function buildWheel(prizeList) {
    if (!wheelSvg) return;
    prizes = (prizeList || []).slice();
    const n = prizes.length;
    if (!n) return;

    // ===== Gradient parser (CSS linear-gradient → SVG linearGradient) =====
    let _gradCount = 0;
    let gradDefs = '';
    function splitTopComma(str) {
      const out = []; let buf = ''; let depth = 0;
      for (let i = 0; i < str.length; i++) {
        const c = str[i];
        if (c === '(') depth++;
        else if (c === ')') depth--;
        if (c === ',' && depth === 0) { out.push(buf); buf = ''; }
        else buf += c;
      }
      if (buf) out.push(buf);
      return out;
    }
    function colorToSvgFill(colorStr) {
      if (!colorStr) return '#1e3a8a';
      const s = String(colorStr).trim();
      // Solid color → return as-is (hex, rgb, hsl, named)
      if (!/^linear-gradient\s*\(/i.test(s)) return s;
      const m = /^linear-gradient\s*\(([\s\S]+)\)\s*$/i.exec(s);
      if (!m) return '#1e3a8a';
      const args = splitTopComma(m[1]);
      let angle = 180, stopsStart = 0;
      const first = (args[0] || '').trim();
      if (/(deg|rad|grad|turn)\s*$/i.test(first)) {
        let v = parseFloat(first);
        if (/rad\s*$/i.test(first))       v = v * 180 / Math.PI;
        else if (/turn\s*$/i.test(first)) v = v * 360;
        else if (/grad\s*$/i.test(first)) v = v * 0.9;
        angle = v; stopsStart = 1;
      } else if (/^to\s+/i.test(first)) {
        const dir = first.toLowerCase().replace(/^to\s+/, '').replace(/\s+/g, ' ').trim();
        const dm = { 'top':0,'right':90,'bottom':180,'left':270,
          'top right':45,'right top':45,'bottom right':135,'right bottom':135,
          'bottom left':225,'left bottom':225,'top left':315,'left top':315 };
        if (dir in dm) angle = dm[dir];
        stopsStart = 1;
      }
      // Convert CSS angle → SVG objectBoundingBox coords
      const rad = (angle - 90) * Math.PI / 180;
      const dx = Math.cos(rad), dy = Math.sin(rad);
      const x1 = (0.5 - dx/2).toFixed(4);
      const y1 = (0.5 - dy/2).toFixed(4);
      const x2 = (0.5 + dx/2).toFixed(4);
      const y2 = (0.5 + dy/2).toFixed(4);
      // Parse color stops
      const stops = [];
      for (let i = stopsStart; i < args.length; i++) {
        const a = args[i].trim();
        const pm = /(\s+-?\d+(?:\.\d+)?%?\s*)$/.exec(a);
        let color = a, pos = '';
        if (pm) { color = a.substring(0, pm.index).trim(); pos = pm[1].trim(); }
        if (color) stops.push({ color, pos });
      }
      if (stops.length < 2) return (stops[0] && stops[0].color) || '#1e3a8a';
      // Auto-distribute missing positions
      for (let i = 0; i < stops.length; i++) {
        if (!stops[i].pos) stops[i].pos = (i / (stops.length - 1) * 100).toFixed(1) + '%';
      }
      const id = 'wgr_' + (++_gradCount);
      const stopsHtml = stops.map(function (st) {
        return '<stop offset="' + st.pos + '" stop-color="' + st.color + '"/>';
      }).join('');
      gradDefs += '<linearGradient id="' + id + '" x1="' + x1 + '" y1="' + y1 + '" x2="' + x2 + '" y2="' + y2 + '">' + stopsHtml + '</linearGradient>';
      return 'url(#' + id + ')';
    }

    const ANIM_PRESETS = {
      'rainbow': {
        defs: '<linearGradient id="anim_rainbow" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="500" y2="0">'
            + '<stop offset="0%" stop-color="#ff0055"/>'
            + '<stop offset="20%" stop-color="#ffcc00"/>'
            + '<stop offset="40%" stop-color="#00ff66"/>'
            + '<stop offset="60%" stop-color="#00c8ff"/>'
            + '<stop offset="80%" stop-color="#7a00ff"/>'
            + '<stop offset="100%" stop-color="#ff0055"/>'
            + '<animateTransform attributeName="gradientTransform" type="translate" from="0 0" to="-500 0" dur="3s" repeatCount="indefinite"/>'
            + '</linearGradient>',
        overlay: function (d) {
          return '<path d="' + d + '" fill="none" stroke="url(#anim_rainbow)" stroke-width="6" stroke-linejoin="round" pointer-events="none"/>';
        }
      },
      'gold-glow': {
        defs: '<filter id="anim_glow_gold" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur in="SourceGraphic" stdDeviation="4"/></filter>',
        overlay: function (d) {
          return '<path d="' + d + '" fill="none" stroke="#ffd700" stroke-width="7" filter="url(#anim_glow_gold)" opacity="0.7" pointer-events="none">'
               + '<animate attributeName="opacity" values="0.3;1;0.3" dur="1.5s" repeatCount="indefinite"/>'
               + '</path>'
               + '<path d="' + d + '" fill="none" stroke="#ffeb70" stroke-width="2.5" pointer-events="none"/>';
        }
      },
      'cyan-glow': {
        defs: '<filter id="anim_glow_cyan" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur in="SourceGraphic" stdDeviation="4"/></filter>',
        overlay: function (d) {
          return '<path d="' + d + '" fill="none" stroke="#00e6ff" stroke-width="7" filter="url(#anim_glow_cyan)" opacity="0.7" pointer-events="none">'
               + '<animate attributeName="opacity" values="0.3;1;0.3" dur="1.5s" repeatCount="indefinite"/>'
               + '</path>'
               + '<path d="' + d + '" fill="none" stroke="#a3f0ff" stroke-width="2.5" pointer-events="none"/>';
        }
      },
      'red-pulse': {
        defs: '',
        overlay: function (d) {
          return '<path d="' + d + '" fill="none" stroke="#ff3344" stroke-width="5" pointer-events="none">'
               + '<animate attributeName="stroke-width" values="3;7;3" dur="1s" repeatCount="indefinite"/>'
               + '<animate attributeName="opacity" values="0.6;1;0.6" dur="1s" repeatCount="indefinite"/>'
               + '</path>';
        }
      },
      'flow-dash': {
        defs: '',
        overlay: function (d) {
          return '<path d="' + d + '" fill="none" stroke="#ffea00" stroke-width="5" stroke-dasharray="15 8" stroke-linecap="round" pointer-events="none">'
               + '<animate attributeName="stroke-dashoffset" from="0" to="-46" dur="0.8s" repeatCount="indefinite"/>'
               + '</path>';
        }
      },
      'purple-aura': {
        defs: '<filter id="anim_aura_purple" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur in="SourceGraphic" stdDeviation="6"/></filter>',
        overlay: function (d) {
          return '<path d="' + d + '" fill="none" stroke="#a855f7" stroke-width="8" filter="url(#anim_aura_purple)" pointer-events="none">'
               + '<animate attributeName="opacity" values="0.3;0.9;0.3" dur="2s" repeatCount="indefinite"/>'
               + '<animate attributeName="stroke-width" values="6;12;6" dur="2s" repeatCount="indefinite"/>'
               + '</path>'
               + '<path d="' + d + '" fill="none" stroke="#d8b4fe" stroke-width="2" pointer-events="none"/>';
        }
      }
    };
    const usedAnims = {};        // key → true (untuk emit defs)
    const pendingAnims = [];     // array of { name, pathD }

    const segAngle = 360 / n;
    const cx = 250, cy = 250, r = 245;
    let segments = '';
    for (let i = 0; i < n; i++) {
      const a1 = (i * segAngle - 90) * Math.PI / 180;
      const a2 = ((i + 1) * segAngle - 90) * Math.PI / 180;
      const x1 = cx + r * Math.cos(a1);
      const y1 = cy + r * Math.sin(a1);
      const x2 = cx + r * Math.cos(a2);
      const y2 = cy + r * Math.sin(a2);
      const large = segAngle > 180 ? 1 : 0;
      const p = prizes[i];
      const fill = colorToSvgFill(p.color);  // v5: parse gradient
      const pathD = `M${cx},${cy} L${x1},${y1} A${r},${r} 0 ${large},1 ${x2},${y2} Z`;
      segments += `<path d="${pathD}" fill="${fill}" stroke="rgba(0,0,0,0.3)" stroke-width="1.5"/>`;
      // v6: collect animation overlay
      if (p.animation && ANIM_PRESETS[p.animation]) {
        usedAnims[p.animation] = true;
        pendingAnims.push({ name: p.animation, pathD: pathD });
      }
    }
    segments += `<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="rgba(0,0,0,0.45)" stroke-width="2"/>`;
    // v6: render animation overlays SETELAH base segments + outer ring (paling depan)
    let animOverlayHtml = '';
    let animDefsHtml = '';
    for (const animKey in usedAnims) {
      if (ANIM_PRESETS[animKey] && ANIM_PRESETS[animKey].defs) {
        animDefsHtml += ANIM_PRESETS[animKey].defs;
      }
    }
    pendingAnims.forEach(function (it) {
      animOverlayHtml += ANIM_PRESETS[it.name].overlay(it.pathD);
    });
    segments += animOverlayHtml;
    // v5+v6: prepend gradient defs + animation defs ke SVG
    if (gradDefs || animDefsHtml) segments = '<defs>' + gradDefs + animDefsHtml + '</defs>' + segments;
    wheelSvg.innerHTML = segments;

    wheelImages.innerHTML = '';
    for (let i = 0; i < n; i++) {
      const img = document.createElement('img');
      img.className = 'prize-img';
      img.src = prizes[i].image;
      img.alt = prizes[i].name;
      img.loading = 'lazy';
      wheelImages.appendChild(img);
    }
    placeImages(currentRotation);
  }

  function placeImages(rotationDeg) {
    if (!prizes.length || !wheelImages) return;
    const n = prizes.length;
    const segAngle = 360 / n;
    const imgs = wheelImages.querySelectorAll('.prize-img');
    const orbitPct = 33;
    imgs.forEach((img, i) => {
      const a = i * segAngle + segAngle / 2 + rotationDeg;
      const rad = (a - 90) * Math.PI / 180;
      const xPct = 50 + Math.cos(rad) * orbitPct;
      const yPct = 50 + Math.sin(rad) * orbitPct;
      img.style.left = xPct + '%';
      img.style.top  = yPct + '%';
    });
  }

  // ---------- SPIN ANIMATION (dramatic) ----------
  // easeOutQuint: more dramatic deceleration than quart
  function easeOutQuint(t) { return 1 - Math.pow(1 - t, 5); }

  function spinTo(targetIndex, prizeData) {
    if (isSpinning) return;
    isSpinning = true;
    const n = prizes.length;
    const segAngle = 360 / n;  // 30deg for 12 segs

    const offsetRange = segAngle * 0.40;
    const offset = (Math.random() * 2 - 1) * offsetRange;

    // More turns + longer duration for "greget" feel
    const turns = 8 + Math.floor(Math.random() * 4);  // 8..11
    const duration = 7200 + Math.floor(Math.random() * 1300);  // 7.2..8.5s

    const targetAt = -(targetIndex * segAngle + segAngle / 2) + offset;
    const start = currentRotation;
    // Normalize so we always spin forward by (turns * 360) + delta
    const baseAngle = start % 360;
    const delta = ((targetAt - baseAngle) % 360 + 360) % 360;
    const endRot = start + turns * 360 + delta;
    const t0 = performance.now();

    function step(now) {
      const t = Math.min(1, (now - t0) / duration);
      const e = easeOutQuint(t);
      const rot = start + (endRot - start) * e;
      currentRotation = rot;
      wheelSvg.style.transform = `rotate(${rot}deg)`;
      placeImages(rot);
      if (t < 1) {
        requestAnimationFrame(step);
      } else {

        settleWheel(endRot, () => {

          setTimeout(() => {
            isSpinning = false;
            showWin(prizeData);

            pollWinners();

            if (balanceRevealTimer) clearTimeout(balanceRevealTimer);
            balanceRevealTimer = setTimeout(function () {
              suppressBalanceUI = false;
              updateUserBalanceUI();
            }, 5000);
          }, 280);
        });
      }
    }
    requestAnimationFrame(step);
  }

  function settleWheel(restRotation, done) {
    const amp = 0.8;              // degrees
    const oscillations = 3;       // half-cycles
    const dur = 700;              // ms
    const t0 = performance.now();
    function step(now) {
      const t = Math.min(1, (now - t0) / dur);
      // damped sine: A * sin(2π * oscillations * t) * (1 - t)^2
      const decay = (1 - t) * (1 - t);
      const wobble = amp * Math.sin(Math.PI * 2 * oscillations * t) * decay;
      const rot = restRotation + wobble;
      currentRotation = rot;
      wheelSvg.style.transform = `rotate(${rot}deg)`;
      placeImages(rot);
      if (t < 1) requestAnimationFrame(step);
      else {
        currentRotation = restRotation;
        wheelSvg.style.transform = `rotate(${restRotation}deg)`;
        placeImages(restRotation);
        if (done) done();
      }
    }
    requestAnimationFrame(step);
  }

  // ---------- WIN MODAL ----------
  function showWin(prize) {
    if (!winModal) return;
    winImg.src = prize.image;
    winImg.alt = prize.name;
    winName.textContent = prize.name;
    const type = prize.type || 'coupon';
    if (type === 'zonk') {
      winBadge.textContent = 'YAH ZONK! 😬';
      winSub.textContent = 'Tenang, kamu dapat 1.000 poin Zonk!';
      if (winClaim) winClaim.style.display = 'none';
    } else if (type === 'coupon') {
      winBadge.textContent = `SELAMAT! +${prize.value} COUPON 🎉`;
      winSub.textContent = 'Coupon masuk ke saldomu';
      if (winClaim) winClaim.style.display = 'none';
    } else {
      winBadge.textContent = 'SELAMAT! 🎉';
      winSub.textContent = 'Kamu memenangkan';
      if (winClaim) winClaim.style.display = '';
    }
    winModal.classList.add('show');
    if (type !== 'zonk') fireConfetti(type === 'coupon' ? 2 : 4);
  }
  function hideWin() {
    if (winModal) winModal.classList.remove('show');

    setTimeout(function(){
      if (typeof pollMe === 'function') pollMe();
    }, 100);
  }

  // ---------- CONFETTI ----------
  let confettiCanvas = null, confettiCtx = null, confettiParticles = [], confettiFrameId = null, confettiResize = null;
  function spawnBurst(count) {
    if (!confettiCanvas) return;
    const colors = ['#f5c842','#ffd96b','#e6a722','#ffffff','#ff6b6b','#16a34a','#3b82f6','#a855f7','#ec4899'];
    const cx = confettiCanvas.width / 2;
    const cy = confettiCanvas.height / 2 - 50;
    for (let i = 0; i < count; i++) {
      confettiParticles.push({
        x: cx + (Math.random()-0.5)*240, y: cy + (Math.random()-0.5)*60,
        vx: (Math.random()-0.5)*16, vy: -10 - Math.random()*16,
        size: 5 + Math.random()*7,
        color: colors[Math.floor(Math.random()*colors.length)],
        rot: Math.random()*Math.PI*2, vr: (Math.random()-0.5)*0.35,
        life: 1, shape: Math.random() < 0.3 ? 'circle' : 'rect'
      });
    }
  }
  function fireConfetti(bursts) {
    bursts = bursts || 3;
    if (!confettiCanvas) {
      confettiCanvas = document.createElement('canvas');
      confettiCanvas.className = 'confetti-canvas';
      document.body.appendChild(confettiCanvas);
      confettiCtx = confettiCanvas.getContext('2d');
      confettiResize = () => { confettiCanvas.width = innerWidth; confettiCanvas.height = innerHeight; };
      confettiResize();
      window.addEventListener('resize', confettiResize);
    }
    for (let b = 0; b < bursts; b++) setTimeout(() => spawnBurst(140), b * 650);
    if (!confettiFrameId) {
      const tick = () => {
        confettiCtx.clearRect(0, 0, confettiCanvas.width, confettiCanvas.height);
        let alive = false;
        for (const p of confettiParticles) {
          if (p.life <= 0 || p.y > confettiCanvas.height + 40) continue;
          p.vy += 0.32; p.x += p.vx; p.y += p.vy; p.rot += p.vr; p.life -= 0.0045;
          alive = true;
          confettiCtx.save();
          confettiCtx.translate(p.x, p.y); confettiCtx.rotate(p.rot);
          confettiCtx.globalAlpha = Math.max(0, p.life);
          confettiCtx.fillStyle = p.color;
          if (p.shape === 'circle') { confettiCtx.beginPath(); confettiCtx.arc(0,0,p.size/2,0,Math.PI*2); confettiCtx.fill(); }
          else confettiCtx.fillRect(-p.size/2, -p.size/2, p.size, p.size*0.5);
          confettiCtx.restore();
        }
        if (alive) confettiFrameId = requestAnimationFrame(tick);
        else {
          cancelAnimationFrame(confettiFrameId); confettiFrameId = null;
          if (confettiResize) window.removeEventListener('resize', confettiResize);
          if (confettiCanvas) confettiCanvas.remove();
          confettiCanvas = null; confettiCtx = null; confettiParticles = []; confettiResize = null;
        }
      };
      confettiFrameId = requestAnimationFrame(tick);
    }
  }

  // ---------- AUTH ----------
  function openLogin() {
    if (!loginModal) return;
    if (loginErrEl) loginErrEl.style.display = 'none';
    if (loginUser) loginUser.value = '';
    if (loginPass) loginPass.value = '';
    loginModal.classList.add('show');
    setTimeout(() => loginUser && loginUser.focus(), 200);
  }
  function closeLogin() { if (loginModal) loginModal.classList.remove('show'); }

  async function handleLogin(e) {
    e.preventDefault();
    const username = loginUser.value.trim();
    const password = loginPass.value;
    if (!username || !password) {
      loginErrEl.textContent = 'Username dan password wajib diisi.';
      loginErrEl.style.display = '';
      return;
    }
    loginSubmit.disabled = true;
    loginSubmit.textContent = 'MASUK...';
    try {
      const res = await fetch('/api/auth/login.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password })
      });
      const data = await res.json();
      if (data.ok) {
        me = data.user;
        closeLogin();
        showToast('Selamat datang!', 'Halo, ' + me.username, 'success');
        setTimeout(() => location.reload(), 600);
      } else {
        loginErrEl.textContent = data.message || 'Login gagal.';
        loginErrEl.style.display = '';
      }
    } catch (err) {
      loginErrEl.textContent = 'Koneksi gagal. Coba lagi.';
      loginErrEl.style.display = '';
    } finally {
      loginSubmit.disabled = false;
      loginSubmit.textContent = 'MASUK';
    }
  }

  async function handleLogout() {
    if (!confirm('Logout dari akun?')) return;
    try { await fetch('/api/auth/logout.php', { method: 'POST' }); } catch (e) {}
    me = null;
    location.reload();
  }

  function updateUserBalanceUI() {
    const c = me ? me.coupons : null;
    const z = me ? me.zonk_balance : null;
    const upC = document.getElementById('upCoupon');
    const upZ = document.getElementById('upZonk');
    if (upC) upC.textContent = c ?? 0;
    if (upZ) upZ.textContent = z ?? 0;
    const elBC = document.getElementById('balCoupon');
    const elBZ = document.getElementById('balZonk');
    if (elBC) elBC.textContent = c ?? 0;
    if (elBZ) elBZ.textContent = z ?? 0;

    const navC = document.getElementById('navCoupon');
    const navZ = document.getElementById('navZonk');
    if (navC) navC.textContent = c ?? 0;
    if (navZ) navZ.textContent = z ?? 0;
    const topBal = document.getElementById('userBalanceTop');
    if (topBal && me) {
      topBal.innerHTML = `<span class="bal-pill"><span class="bal-icon">🎟️</span><b id="balCoupon">${me.coupons}</b> coupon</span>
                          <span class="bal-pill"><span class="bal-icon">😬</span><b id="balZonk">${me.zonk_balance}</b> zonk</span>`;
    } else if (topBal && !me) {
      topBal.innerHTML = '<span class="bal-muted">Login untuk lihat saldo</span>';
    }
  }

  // ---------- SPIN BUTTON STATE ----------
  function updateSpinButton() {
    if (!spinBtn) return;
    if (!me) {
      spinBtn.classList.remove('ready');
      spinBtn.disabled = false;
      spinBtn.dataset.mode = 'login';
      spinBtnSub.textContent = 'Login dulu untuk spin';
      return;
    }
    if (me.spin_available > 0) {
      spinBtn.classList.add('ready');
      spinBtn.disabled = false;
      spinBtn.dataset.mode = 'spin';
      spinBtnSub.textContent = me.spin_available + 'x spin tersedia';
    } else {
      spinBtn.classList.remove('ready');
      spinBtn.disabled = true;
      spinBtn.dataset.mode = 'wait';
      spinBtnSub.textContent = 'Belum ada spin — hubungi admin';
    }
  }

  async function handleSpin() {
    if (isSpinning) return;
    if (!me) { openLogin(); return; }
    if (me.spin_available <= 0) {
      showToast('Belum ada spin', 'Capai TO atau hubungi admin', 'error');
      return;
    }
    spinBtn.disabled = true;

    unlockAudio();
    try {
      const res = await fetch('/api/spin3/spin.php', { method: 'POST' });
      const textBody = await res.text();
      let data;
      try {
        data = JSON.parse(textBody);
      } catch (parseErr) {
        console.error('[SPIN] non-JSON response:', textBody.slice(0, 500));
        showToast('Server Error', 'HTTP ' + res.status + '. Cek /spin-test.php', 'error');
        spinBtn.disabled = false;
        return;
      }
      if (!data.ok) {
        const stepInfo = data.step ? ' [' + data.step + ']' : '';
        const detailInfo = data.error_detail ? ' — ' + data.error_detail : '';
        showToast('Gagal Spin', (data.message || 'Tidak bisa spin') + stepInfo + detailInfo, 'error');
        spinBtn.disabled = false;
        return;
      }
      me = data.user;

      suppressBalanceUI = true;
      if (balanceRevealTimer) { clearTimeout(balanceRevealTimer); balanceRevealTimer = null; }
      updateSpinButton();
      try {

        playSpinSound((data.prize && data.prize.type) || 'coupon');
        spinTo(data.prize_index, data.prize);
      } catch (spinErr) {

        suppressBalanceUI = false;
        updateUserBalanceUI();
        showToast('Animasi Error', spinErr.message, 'error');
        spinBtn.disabled = false;
      }
    } catch (e) {
      showToast('Koneksi Gagal', e.message || 'Network error', 'error');
      spinBtn.disabled = false;
    }
  }

  function sortShopItems(items) {
    const arr = (items || []).slice();
    if (shopSort === 'default') return arr;
    // v-makanan: filter khusus Makanan (category === 'food')
    if (shopSort === 'food') return arr.filter(i => (i.category || '') === 'food');
    // Poin Zonk: hanya item tipe zonk_cash
    if (shopSort === 'zonk') return arr.filter(i => i.type === 'zonk_cash');
    const coupon = arr.filter(i => i.type !== 'zonk_cash');
    const zonk   = arr.filter(i => i.type === 'zonk_cash');
    coupon.sort((a, b) => shopSort === 'cheap'
      ? (a.coupon_price || 0) - (b.coupon_price || 0)
      : (b.coupon_price || 0) - (a.coupon_price || 0));
    return coupon.concat(zonk);
  }

  // Set/refresh sumber data toko lalu render ulang (menjaga halaman & filter aktif).
  function setShopItems(items) {
    shopAllItems = Array.isArray(items) ? items : [];
    bindShopFilter();
    renderShop();
  }

  function renderShop() {
    if (!shopGrid) return;
    const sorted = sortShopItems(shopAllItems);
    const total = sorted.length;
    const totalPages = Math.max(1, Math.ceil(total / SHOP_PER_PAGE));
    if (shopPage > totalPages) shopPage = totalPages;
    if (shopPage < 1) shopPage = 1;
    const startIdx = (shopPage - 1) * SHOP_PER_PAGE;
    const pageItems = sorted.slice(startIdx, startIdx + SHOP_PER_PAGE);

    shopGrid.innerHTML = pageItems.map(item => {
      const stock = item.stock == null ? -1 : item.stock;
      const stockText = stock === -1 ? '<span class="shop-stock unlim">∞</span>'
                      : stock > 0 ? `<span class="shop-stock">Stock: ${stock}</span>`
                      : '<span class="shop-stock zero">Habis</span>';
      const price = item.type === 'zonk_cash'
                  ? `<span class="shop-price zonk">${item.zonk_price} Zonk</span>`
                  : `<span class="shop-price">${item.coupon_price} Coupon</span>`;
      const isFood = (item.category || '') === 'food'; // v-makanan
      const isPremium = (!isFood && item.type !== 'zonk_cash' && (item.coupon_price || 0) >= 300); // v-premium: coupon >= 300
      const ribbon = isFood ? '<div class="makanan-ribbon"></div>'
                   : isPremium ? '<div class="premium-ribbon"></div>' : '';
      return `<div class="shop-card${isFood ? ' food-card' : ''}" data-id="${escapeHtml(item.id)}" data-type="${escapeHtml(item.type)}" data-category="${escapeHtml(item.category||'')}" data-price="${item.coupon_price||0}" data-zonk="${item.zonk_price||0}" data-stock="${stock}" data-name="${escapeHtml(item.name)}" data-image="${escapeHtml(item.image)}">
        ${ribbon}
        <div class="shop-card-img"><img src="${escapeHtml(item.image)}" alt="${escapeHtml(item.name)}" loading="lazy" onerror="this.onerror=null;this.style.display='none';this.parentElement.classList.add('img-missing');"></div>
        <div class="shop-card-name">${escapeHtml(item.name)}</div>
        <div class="shop-card-meta">${price}${stockText}</div>
        <button class="shop-btn" type="button"${stock === 0 ? ' disabled' : ''}>TUKAR</button>
      </div>`;
    }).join('');
    bindShopButtons();
    renderShopPager(totalPages);
    updateShopFilterUI();
  }

  // Window nomor halaman: 1 … (cur-1 .. cur+1) … last
  function renderShopPager(totalPages) {
    if (!shopPager) return;
    if (totalPages <= 1) { shopPager.innerHTML = ''; return; }
    const win = 1, parts = [];
    const start = Math.max(1, shopPage - win);
    const end   = Math.min(totalPages, shopPage + win);
    const btn = (p) => `<button type="button" class="${p === shopPage ? 'current' : ''}" data-page="${p}">${p}</button>`;
    parts.push(`<button type="button" data-page="${shopPage - 1}"${shopPage <= 1 ? ' disabled' : ''}>‹</button>`);
    if (start > 1) { parts.push(btn(1)); if (start > 2) parts.push('<span class="pg-dots">…</span>'); }
    for (let p = start; p <= end; p++) parts.push(btn(p));
    if (end < totalPages) { if (end < totalPages - 1) parts.push('<span class="pg-dots">…</span>'); parts.push(btn(totalPages)); }
    parts.push(`<button type="button" data-page="${shopPage + 1}"${shopPage >= totalPages ? ' disabled' : ''}>›</button>`);
    shopPager.innerHTML = parts.join('');
    shopPager.querySelectorAll('button[data-page]').forEach(b => {
      b.addEventListener('click', () => {
        const p = parseInt(b.getAttribute('data-page'), 10);
        if (!isNaN(p)) { shopPage = p; renderShop(); shopGrid.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
      });
    });
  }

  function updateShopFilterUI() {
    if (!shopFilter) return;
    shopFilter.querySelectorAll('.shop-filter-btn').forEach(b => {
      b.classList.toggle('active', b.getAttribute('data-sort') === shopSort);
    });
  }

  function bindShopFilter() {
    if (!shopFilter || shopFilter.dataset.bound === '1') return;
    shopFilter.dataset.bound = '1';
    shopFilter.querySelectorAll('.shop-filter-btn').forEach(b => {
      b.addEventListener('click', () => {
        shopSort = b.getAttribute('data-sort') || 'default';
        shopPage = 1;
        renderShop();
      });
    });
  }

  function bindShopButtons() {
    if (!shopGrid) return;
    const buttons = shopGrid.querySelectorAll('.shop-card .shop-btn, .shop-card .modal-btn-redeem, .shop-card button[data-redeem]');
    buttons.forEach(btn => {
      if (btn.dataset.bound === '1') return;
      btn.dataset.bound = '1';
      btn.addEventListener('click', (ev) => {
        ev.preventDefault();
        ev.stopPropagation();
        const card = btn.closest('.shop-card');
        if (!card) return;
        if (!me) { openLogin(); return; }
        const item = {
          id:    card.dataset.id || btn.dataset.redeem || '',
          type:  card.dataset.type || 'coupon',
          price: parseInt(card.dataset.price, 10) || 0,
          zonk:  parseInt(card.dataset.zonk, 10)  || 0,
          name:  card.dataset.name  || (card.querySelector('.shop-card-name,.shop-name') && card.querySelector('.shop-card-name,.shop-name').textContent) || '',
          image: card.dataset.image || (card.querySelector('img') && card.querySelector('img').src) || '',
          btnElement: btn
        };
        console.log('[TUKAR] clicked item:', item);
        // Pre-check balance before opening modal (cleaner UX)
        if (item.type === 'zonk_cash') {
          if ((me.zonk_balance || 0) < item.zonk) {
            showToast('Zonk kurang', `Butuh ${item.zonk} Zonk (kamu punya ${me.zonk_balance || 0})`, 'error');
            return;
          }
        } else {
          if ((me.coupons || 0) < item.price) {
            showToast('Coupon kurang', `Butuh ${item.price} Coupon (kamu punya ${me.coupons || 0})`, 'error');
            return;
          }
        }
        openConfirmRedeem(item);
      });
    });
    console.log('[TUKAR] bindShopButtons: bound ' + buttons.length + ' buttons');
  }

  function openConfirmRedeem(item) {
    pendingRedeemItem = item;
    if (confirmImg)  confirmImg.src = item.image;
    if (confirmName) confirmName.textContent = item.name;
    if (confirmPrice) {
      confirmPrice.textContent = item.type === 'zonk_cash'
        ? `${item.zonk} Zonk`
        : `${item.price} Coupon`;
      confirmPrice.className = 'confirm-item-price' + (item.type === 'zonk_cash' ? ' zonk' : '');
    }
    if (confirmModal) confirmModal.classList.add('show');
  }
  function closeConfirmRedeem() {
    if (confirmModal) confirmModal.classList.remove('show');
    pendingRedeemItem = null;
  }

  async function executeRedeem() {
    if (!pendingRedeemItem) return;
    const item = pendingRedeemItem;
    const btn = item.btnElement;
    closeConfirmRedeem();
    if (btn) { btn.disabled = true; btn.textContent = '...'; }
    try {
      const res = await fetch('/api/spin3/redeem-shop.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ item_id: item.id })
      });
      const data = await res.json();
      if (data.ok) {
        if (data.user) { me = data.user; updateUserBalanceUI(); updateSpinButton(); }
        showToast('Sukses!', data.message || 'Permintaan dikirim ke admin', 'success');
      } else {
        showToast('Gagal', data.message || 'Tukar gagal', 'error');
      }
    } catch (err) {
      showToast('Error', 'Koneksi gagal', 'error');
    } finally {
      if (btn) { btn.disabled = false; btn.textContent = 'TUKAR'; }
    }
  }

  // ---------- WINNERS: sliding window with flash on new ----------
  function renderWinners(list, animate) {

    let targets = Array.from(document.querySelectorAll('.winners-list'));
    if (!targets.length && winnersEl) targets = [winnersEl];
    if (!targets.length) return;

    if (!list || !list.length) {
      targets.forEach(el => {
        el.innerHTML = '<div class="winners-empty">Belum ada pemenang.</div>';
      });
      knownWinnerIds.clear();
      return;
    }

    // Limit to 8
    list = list.slice(0, 8);

    const incomingIds = list.map(w => w.id).filter(Boolean);
    const newIds = animate ? incomingIds.filter(id => !knownWinnerIds.has(id)) : [];

    const htmlContent = list.map((w, i) => {
      const isNew = newIds.includes(w.id);
      return `
        <div class="winner-row${isNew ? ' is-new' : ''}" data-wid="${escapeHtml(w.id)}">
          <div class="winner-img"><img src="${escapeHtml(w.image)}" alt="" loading="lazy"></div>
          <div class="winner-info">
            <b>${escapeHtml(w.username_c)}</b>
            <span>${escapeHtml(w.prize_name)}</span>
          </div>
          <span class="winner-time ${i === 0 ? 'now' : ''}">${escapeHtml(w.time_ago)}</span>
        </div>`;
    }).join('');

    targets.forEach(el => { el.innerHTML = htmlContent; });

    // Update known set: only keep IDs still in the visible window
    knownWinnerIds.clear();
    incomingIds.forEach(id => knownWinnerIds.add(id));

    // Strip .is-new class after animation completes
    if (newIds.length) {
      setTimeout(() => {
        targets.forEach(el => {
          el.querySelectorAll('.winner-row.is-new').forEach(node => node.classList.remove('is-new'));
        });
      }, 1600);
    }
  }

  // ---------- F / I / CONFIRM MODAL HANDLERS ----------
  function openCara() { if (caraModal) caraModal.classList.add('show'); }
  function closeCara(){ if (caraModal) caraModal.classList.remove('show'); }
  function openFaq()  { if (faqModal)  faqModal.classList.add('show'); }
  function closeFaq() { if (faqModal)  faqModal.classList.remove('show'); }

  function bindFaqAccordion() {
    if (!faqModal) return;
    faqModal.querySelectorAll('.faq-item .faq-q').forEach(btn => {
      btn.addEventListener('click', () => {
        const item = btn.parentElement;
        const wasOpen = item.classList.contains('open');
        // Close all, open clicked (accordion behavior)
        faqModal.querySelectorAll('.faq-item.open').forEach(el => el.classList.remove('open'));
        if (!wasOpen) item.classList.add('open');
      });
    });
  }

  // ---------- POLLING ----------
  async function pollPrizes() {
    if (!HAS_WHEEL) return;
    try {
      const res = await fetch('/api/spin3/prizes.php', { cache: 'no-store' });
      const data = await res.json();
      if (!data.ok) return;
      const wheelKey = JSON.stringify(data.prizes.map(p => ({i:p.id,c:p.color,a:p.animation,n:p.name,im:p.image,o:p.order})));
      const wh = hash(wheelKey);
      if (wh !== lastWheelHash && !isSpinning) {
        lastWheelHash = wh;
        buildWheel(data.prizes);
      }
      if (totalEl) totalEl.textContent = fmt(data.total_prizes);
      if (heroSubEl && data.hero_subtitle) heroSubEl.textContent = data.hero_subtitle;
      if ((data.floating_html || '') !== lastFloatingHTML) {
        lastFloatingHTML = data.floating_html || '';
        if (floatSlot) floatSlot.innerHTML = lastFloatingHTML;
      }
    } catch (e) {}
  }

  async function pollWinners() {
    if (!HAS_WHEEL) return;

    if (isSpinning) return;
    try {
      const res = await fetch('/api/spin3/winners.php?limit=8', { cache: 'no-store' });
      // Re-check after the await — a spin may have started while fetch was in flight.
      if (isSpinning) return;
      const data = await res.json();
      if (!data.ok) return;
      if (isSpinning) return;
      const h = hash(JSON.stringify((data.winners||[]).map(w => w.id + ':' + w.time_ago)));
      if (h !== lastWinnersHash) {
        const isInitial = lastWinnersHash === '';
        lastWinnersHash = h;
        renderWinners(data.winners, !isInitial);
      }
    } catch (e) {}
  }

  async function pollShop() {
    if (!HAS_WHEEL) return;
    try {
      const res = await fetch('/api/spin3/shop.php', { cache: 'no-store' });
      const data = await res.json();
      if (!data.ok) return;
      const h = hash(JSON.stringify(data.items.map(i => i.id + ':' + i.stock + ':' + i.coupon_price + ':' + i.name)));
      if (h !== lastShopHash) {
        lastShopHash = h;
        setShopItems(data.items);
      }
    } catch (e) {}
  }

  async function pollMe() {

    if (isSpinning || suppressBalanceUI) return;
    try {
      const res = await fetch('/api/spin3/me.php', { cache: 'no-store' });
      const data = await res.json();
      if (!data.ok) return;
      const newMe = data.user;
      const h = hash(JSON.stringify(newMe));
      if (h !== lastMeHash) {
        lastMeHash = h;
        const wasLoggedIn = me !== null;
        const isLoggedIn  = newMe !== null;
        me = newMe;

        if (wasLoggedIn !== isLoggedIn) renderAuthUI();
        updateUserBalanceUI();
        updateSpinButton();
      }
    } catch (e) {}
  }

  function renderAuthUI() {
    if (!navAuth) return;
    if (me) {
      const initial = (me.username || '?').charAt(0).toUpperCase();
      navAuth.innerHTML =
        '<div class="user-pill" id="userPill">' +
          '<div class="up-avatar">' + escapeHtml(initial) + '</div>' +
          '<div class="up-info">' +
            '<b>' + escapeHtml(me.username) + '</b>' +
            '<span><span class="up-c">🎟️ <b id="upCoupon">' + me.coupons + '</b></span> ' +
            '<span class="up-z">😬 <b id="upZonk">' + me.zonk_balance + '</b></span></span>' +
          '</div>' +
          '<button class="up-logout" type="button" id="logoutBtn" title="Logout">' +
            '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>' +
          '</button>' +
        '</div>';
      const lb = document.getElementById('logoutBtn');
      if (lb) lb.addEventListener('click', handleLogout);
    } else {
      navAuth.innerHTML =
        '<button class="login-btn" type="button" id="openLogin">' +
          '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>' +
          'LOGIN' +
        '</button>';
      const ol = document.getElementById('openLogin');
      if (ol) ol.addEventListener('click', openLogin);
    }
  }

  // ---------- ZOOM LOCK ----------
  function lockZoom() {
    document.addEventListener('gesturestart', e => e.preventDefault());
    document.addEventListener('wheel', e => { if (e.ctrlKey) e.preventDefault(); }, { passive: false });
    document.addEventListener('keydown', e => {
      if ((e.ctrlKey || e.metaKey) && ['+','-','=','0'].includes(e.key)) e.preventDefault();
    });
  }

  // ---------- INIT ----------
  function bindUI() {
    if (loginForm)   loginForm.addEventListener('submit', handleLogin);
    if (loginCancel) loginCancel.addEventListener('click', closeLogin);
    if (loginModal)  loginModal.addEventListener('click', e => { if (e.target === loginModal) closeLogin(); });

    const openLoginBtn = document.getElementById('openLogin');
    if (openLoginBtn) openLoginBtn.addEventListener('click', openLogin);

    const logoutBtn = document.getElementById('logoutBtn');
    if (logoutBtn) logoutBtn.addEventListener('click', handleLogout);

    // History Penukaran (SpinShop) — tombol di kiri username
    const histBtn   = document.getElementById('navHistoryBtn');
    const histClose = document.getElementById('redempClose');
    const histModal = document.getElementById('redempModal');
    if (histBtn)   histBtn.addEventListener('click', openRedemptionHistory);
    if (histClose) histClose.addEventListener('click', closeRedemptionHistory);
    if (histModal) histModal.addEventListener('click', e => { if (e.target === histModal) closeRedemptionHistory(); });

    // Popup isi alamat pengiriman (sisi pemain)
    const shipModalEl  = document.getElementById('shipModal');
    const shipCloseEl  = document.getElementById('shipClose');
    const shipCancelEl = document.getElementById('shipCancel');
    const shipSubmitEl = document.getElementById('shipSubmit');
    if (shipCloseEl)  shipCloseEl.addEventListener('click', closeShipForm);
    if (shipCancelEl) shipCancelEl.addEventListener('click', closeShipForm);
    if (shipSubmitEl) shipSubmitEl.addEventListener('click', submitShipForm);
    if (shipModalEl)  shipModalEl.addEventListener('click', e => { if (e.target === shipModalEl) closeShipForm(); });

    if (spinBtn)  spinBtn.addEventListener('click', handleSpin);
    if (winClose) winClose.addEventListener('click', hideWin);
    if (winClaim) winClaim.addEventListener('click', () => setTimeout(hideWin, 120));
    if (winModal) winModal.addEventListener('click', e => { if (e.target === winModal) hideWin(); });

    if (navToggle) navToggle.addEventListener('click', () => navMenu.classList.toggle('open'));

    // F / I icon handlers
    if (openCaraBtn)  openCaraBtn.addEventListener('click', openCara);
    if (closeCaraBtn) closeCaraBtn.addEventListener('click', closeCara);
    if (caraModal)    caraModal.addEventListener('click', e => { if (e.target === caraModal) closeCara(); });
    if (openFaqBtn)   openFaqBtn.addEventListener('click', openFaq);
    if (closeFaqBtn)  closeFaqBtn.addEventListener('click', closeFaq);
    if (faqModal)     faqModal.addEventListener('click', e => { if (e.target === faqModal) closeFaq(); });
    bindFaqAccordion();

    // Confirm redeem modal
    if (confirmOk)     confirmOk.addEventListener('click', executeRedeem);
    if (confirmCancel) confirmCancel.addEventListener('click', closeConfirmRedeem);
    if (confirmModal)  confirmModal.addEventListener('click', e => { if (e.target === confirmModal) closeConfirmRedeem(); });

    // ESC closes any open modal
    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape') {
        closeLogin(); closeCara(); closeFaq(); closeConfirmRedeem(); hideWin();
        closeRedemptionHistory();
        closeShipForm();
      }
    });

    if (shopGrid) bindShopButtons();
  }

  function start() {
    me = window.__ME__ || null;
    bindUI();
    lockZoom();
    updateUserBalanceUI();
    updateSpinButton();

    if (HAS_WHEEL) {
      // Full mode: init wheel + winners + shop + poll all
      if (window.__INITIAL_PRIZES__ && window.__INITIAL_PRIZES__.length) {
        buildWheel(window.__INITIAL_PRIZES__);
        lastWheelHash = hash(JSON.stringify(window.__INITIAL_PRIZES__.map(p => ({i:p.id,c:p.color,a:p.animation,n:p.name,im:p.image,o:p.order}))));
      }
      if (window.__INITIAL_WINNERS__ && window.__INITIAL_WINNERS__.length) {
        lastWinnersHash = hash(JSON.stringify(window.__INITIAL_WINNERS__.map(w => w.id + ':' + w.time_ago)));
        // Seed known set so initial render doesn't flash
        window.__INITIAL_WINNERS__.forEach(w => { if (w.id) knownWinnerIds.add(w.id); });
      }
      if (window.__INITIAL_SHOP__ && window.__INITIAL_SHOP__.length) {
        lastShopHash = hash(JSON.stringify(window.__INITIAL_SHOP__.map(i => i.id + ':' + i.stock + ':' + i.coupon_price + ':' + i.name)));
        // v-shop: render paginasi/filter dari data awal (server render full → JS bagi per halaman)
        bindShopFilter();
        setShopItems(window.__INITIAL_SHOP__);
      }
    }
    lastMeHash = hash(JSON.stringify(me));

    // v15: BIND SHOP BUTTONS IMMEDIATELY (fix tombol TUKAR)
    bindShopButtons();
    // v15: Setup refresh button
    bindRefreshBtn();

    // Initial poll
    pollPrizes(); pollWinners(); pollShop(); pollMe();
    setTimeout(bindShopButtons, 500);
    setTimeout(bindShopButtons, 2000);

    // Polling interval — slower on subpages (just me) to save bandwidth
    const interval = HAS_WHEEL ? 4000 : 8000;
    pollTimer = setInterval(() => {
      if (HAS_WHEEL) { pollPrizes(); pollWinners(); pollShop(); }
      pollMe();
    }, interval);


    if (document.getElementById('navHistoryBtn')) {
      loadRedempBadgeOnly();
      setInterval(loadRedempBadgeOnly, 12000);
    }
  }

  // ---------- HISTORY PENUKARAN (popup + realtime status) ----------
  let redempPollTimer = null;

  function redempStageEmoji(stage, isFood) {
    if (isFood) return ''; // label makanan sudah membawa emoji sendiri (👨‍🍳/🛵/✅)
    switch (stage) {
      case 'pending':   return '⏳';
      case 'rejected':  return '❌';
      case 'preparing': return '📦';
      case 'ready':     return '✅';
      case 'shipped':   return '🚚';
      case 'delivered': return '🎉';
      case 'cancelled': return '🚫';
      case 'done':      return '✅';
      default:          return '•';
    }
  }

  // Simpan item terakhir supaya popup "Lengkapi Alamat" bisa prefill datanya.
  let lastRedemptions = [];

  function escHtml(s) {
    return String(s == null ? '' : s)
      .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
  }

  function renderRedemptions(items) {
    const list = document.getElementById('redempList');
    if (!list) return;
    lastRedemptions = items || [];
    if (!items || !items.length) {
      list.innerHTML = '<div class="sp-redemp-empty">Belum ada penukaran. Menangkan barang dari spin atau tukar coupon di toko!</div>';
      return;
    }
    list.innerHTML = items.map(function (r) {
      const img = r.item_image
        ? '<img class="sp-redemp-img" src="' + r.item_image + '" alt="" onerror="this.style.visibility=\'hidden\'">'
        : '<div class="sp-redemp-img"></div>';
      let meta = r.source;
      if (r.cost) meta += ' · ' + r.cost;
      meta += ' · ' + r.time_ago;
      let track = '';
      if (r.stage === 'shipped' || r.stage === 'delivered') {
        const parts = [];
        if (r.courier)     parts.push(r.courier);
        if (r.tracking_no) parts.push('Resi: ' + r.tracking_no);
        if (parts.length)  track = '<div class="sp-redemp-track">' + parts.join(' · ') + '</div>';
      }

      // Tombol "Lengkapi Alamat" hanya untuk barang fisik yang sudah di-approve
      // (stage selain pending/rejected/done-non-fisik). Label berubah jadi
      // "Ubah Alamat" kalau pemain sudah pernah mengisi.
      let addrBtn = '';
      const canShip = r.can_ship && ['preparing','ready','shipped','delivered'].indexOf(r.stage) !== -1;
      if (canShip) {
        const sh = r.ship || {};
        const filled = sh.recipient_name && sh.address;
        // Setelah dikirim/diterima, alamat dikunci (tidak bisa diubah pemain).
        const locked = (r.stage === 'shipped' || r.stage === 'delivered');
        if (!locked) {
          const isFoodItem = (r.is_food == 1) || (r.kind === 'shop_food');
          const lbl = isFoodItem
            ? (filled ? '✏️ Ubah Data Pesanan' : '📍 Lengkapi Data Pesanan')
            : (filled ? '✏️ Ubah Alamat' : '📍 Lengkapi Alamat');
          addrBtn = '<button type="button" class="sp-redemp-addr-btn" data-rid="' + escHtml(r.id) + '">' +
                      lbl +
                    '</button>';
        }
      }

      return '' +
        '<div class="sp-redemp-row">' +
          img +
          '<div class="sp-redemp-main">' +
            '<p class="sp-redemp-name">' + (r.item_name || '-') + '</p>' +
            '<div class="sp-redemp-meta">' + meta + '</div>' +
            track +
            addrBtn +
          '</div>' +
          '<span class="sp-redemp-badge" style="background:' + (r.status_color || '#888') + '">' +
            redempStageEmoji(r.stage, r.is_food) + ' ' + (r.status_label || '') +
          '</span>' +
        '</div>';
    }).join('');

    // Bind tombol alamat
    list.querySelectorAll('.sp-redemp-addr-btn').forEach(function (b) {
      b.addEventListener('click', function () {
        openShipForm(b.getAttribute('data-rid'));
      });
    });
  }

  // ---------- POPUP ISI ALAMAT PENGIRIMAN / DATA PESANAN (sisi pemain) ----------
  let currentShipIsFood = false; // v-makanan

  function openShipForm(rid) {
    const r = (lastRedemptions || []).find(function (x) { return String(x.id) === String(rid); });
    if (!r) return;
    const modal = document.getElementById('shipModal');
    if (!modal) return;
    const sh = r.ship || {};
    const isFood = (r.is_food == 1) || (r.kind === 'shop_food');
    currentShipIsFood = isFood;
    const set = function (id, v) { const e = document.getElementById(id); if (e) e.value = v || ''; };
    set('shipRid', r.id);
    set('shipRecipient', sh.recipient_name);
    set('shipPhone1', sh.phone1);
    set('shipPhone2', sh.phone2);
    set('shipAddress', sh.address);
    set('shipLandmark', sh.landmark);
    set('shipNotes', sh.notes);
    set('shipStore', sh.store_name);
    set('shipFood', sh.food_order);

    // Toggle tampilan khusus makanan vs barang fisik
    const show = function (id, on) { const e = document.getElementById(id); if (e) e.style.display = on ? '' : 'none'; };
    show('shipFoodFields', isFood);
    show('shipFoodNote', isFood);
    show('shipLandmarkStar', isFood);
    const titleEl = document.getElementById('shipTitle');
    const subEl   = document.getElementById('shipSubtitle');
    if (titleEl) titleEl.textContent = isFood ? '🍔 Data Pesanan Makanan' : '📦 Data Pengiriman';
    if (subEl)   subEl.textContent   = isFood ? 'Isi data pemesanan makanan. Langsung diteruskan ke admin.'
                                              : 'Isi alamat & kontak. Data langsung dikirim ke admin.';

    const nameEl = document.getElementById('shipItemName');
    if (nameEl) nameEl.textContent = r.item_name || '-';
    const errEl = document.getElementById('shipErr');
    if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
    const subBtn = document.getElementById('shipSubmit');
    if (subBtn) subBtn.textContent = isFood ? '💾 Simpan Pesanan' : '💾 Simpan Alamat';
    modal.style.display = 'flex';
  }

  function closeShipForm() {
    const modal = document.getElementById('shipModal');
    if (modal) modal.style.display = 'none';
  }

  async function submitShipForm() {
    const errEl  = document.getElementById('shipErr');
    const okBtn  = document.getElementById('shipSubmit');
    const val = function (id) { const e = document.getElementById(id); return e ? e.value.trim() : ''; };
    const payload = {
      redemption_id:  val('shipRid'),
      recipient_name: val('shipRecipient'),
      phone1:         val('shipPhone1'),
      phone2:         val('shipPhone2'),
      address:        val('shipAddress'),
      landmark:       val('shipLandmark'),
      notes:          val('shipNotes'),
      store_name:     val('shipStore'),
      food_order:     val('shipFood'),
    };
    if (!payload.recipient_name || !payload.phone1 || !payload.address) {
      if (errEl) { errEl.textContent = 'Nama penerima, No. HP utama, dan alamat lengkap wajib diisi.'; errEl.style.display = 'block'; }
      return;
    }
    if (currentShipIsFood && (!payload.store_name || !payload.food_order || !payload.landmark)) {
      if (errEl) { errEl.textContent = 'Nama toko, pesanan makanan/minuman, dan patokan alamat wajib diisi.'; errEl.style.display = 'block'; }
      return;
    }
    if (okBtn) { okBtn.disabled = true; okBtn.textContent = 'Menyimpan…'; }
    try {
      const res  = await fetch('/api/spin3/shipping-save.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });
      const data = await res.json();
      if (data.ok) {
        closeShipForm();
        if (typeof showToast === 'function') showToast('Tersimpan', data.message || 'Data tersimpan.', 'success');
        loadRedemptions();
        loadRedempBadgeOnly();
      } else {
        if (data.need_login) { closeShipForm(); closeRedemptionHistory(); openLogin(); return; }
        if (errEl) { errEl.textContent = data.message || 'Gagal menyimpan.'; errEl.style.display = 'block'; }
      }
    } catch (e) {
      if (errEl) { errEl.textContent = 'Koneksi gagal. Coba lagi.'; errEl.style.display = 'block'; }
    }
    if (okBtn) { okBtn.disabled = false; okBtn.textContent = currentShipIsFood ? '💾 Simpan Pesanan' : '💾 Simpan Alamat'; }
  }

  // ---------- NOTIFIKASI MERAH DI TOMBOL HISTORY (sisi pemain) ----------
  function getSeenMap() {
    try { return JSON.parse(localStorage.getItem('sp_redemp_seen') || '{}') || {}; }
    catch (e) { return {}; }
  }
  function setSeenMap(m) {
    try { localStorage.setItem('sp_redemp_seen', JSON.stringify(m)); } catch (e) {}
  }
  function markRedempSeen(items) {
    const m = {};
    (items || []).forEach(function (r) { m[r.id] = r.stage; });
    setSeenMap(m);
  }
  // Badge = item yang butuh aksi pemain (isi alamat) + perubahan status hasil
  // (dikirim/diterima/ditolak/dibatalkan) yang belum pernah dilihat pemain.
  function computeRedempBadge(items) {
    if (!items) return 0;
    const seen = getSeenMap();
    let n = 0;
    items.forEach(function (r) {
      if (r.action_needed == 1) { n++; return; }
      const notable = (r.stage === 'shipped' || r.stage === 'delivered' || r.stage === 'rejected' || r.stage === 'cancelled');
      if (notable && seen[r.id] !== r.stage) n++;
    });
    return n;
  }
  function applyRedempBadge(n) {
    const el = document.getElementById('historyBadge');
    if (!el) return;
    if (n > 0) { el.textContent = n > 99 ? '99+' : String(n); el.style.display = ''; }
    else { el.style.display = 'none'; }
  }
  async function loadRedempBadgeOnly() {
    if (!me) { applyRedempBadge(0); return; }
    try {
      const res  = await fetch('/api/spin3/redemptions.php', { cache: 'no-store' });
      const data = await res.json();
      if (data.ok) applyRedempBadge(computeRedempBadge(data.items));
    } catch (e) { /* diam */ }
  }

  async function loadRedemptions() {
    const list = document.getElementById('redempList');
    try {
      const res  = await fetch('/api/spin3/redemptions.php', { cache: 'no-store' });
      const data = await res.json();
      if (!data.ok) {
        if (data.need_login) { closeRedemptionHistory(); openLogin(); return; }
        if (list) list.innerHTML = '<div class="sp-redemp-empty">Gagal memuat history.</div>';
        return;
      }
      renderRedemptions(data.items);
      // Popup terbuka = pemain sudah melihat → tandai status hasil sebagai "seen",
      // tapi item yang masih butuh isi alamat tetap dihitung sampai diisi.
      markRedempSeen(data.items);
      applyRedempBadge(computeRedempBadge(data.items));
    } catch (e) {
      if (list) list.innerHTML = '<div class="sp-redemp-empty">Koneksi gagal. Coba lagi.</div>';
    }
  }

  function openRedemptionHistory() {
    if (!me) { openLogin(); return; }
    const modal = document.getElementById('redempModal');
    if (!modal) return;
    modal.style.display = 'flex';
    loadRedemptions();

    if (redempPollTimer) clearInterval(redempPollTimer);
    redempPollTimer = setInterval(loadRedemptions, 5000);
  }

  function closeRedemptionHistory() {
    const modal = document.getElementById('redempModal');
    if (modal) modal.style.display = 'none';
    if (redempPollTimer) { clearInterval(redempPollTimer); redempPollTimer = null; }
  }

  // v15: REFRESH BUTTON
  async function refreshUserData() {

    if (isSpinning || suppressBalanceUI) {
      showToast('Tunggu sebentar', 'Spin sedang berjalan…', 'info');
      return;
    }
    const btn = document.getElementById('navRefreshBtn');
    if (btn) { btn.classList.add('spinning'); btn.disabled = true; }
    try {
      const res = await fetch('/api/spin3/me.php', { cache: 'no-store' });
      const data = await res.json();
      if (data.ok && data.user) {
        me = data.user;
        updateUserBalanceUI();
        updateSpinButton();
        const u = data.user;
        const navCoupon = document.getElementById('navCoupon');
        const navZonk = document.getElementById('navZonk');
        if (navCoupon) navCoupon.textContent = u.coupons || 0;
        if (navZonk) navZonk.textContent = u.zonk_balance || 0;
        showToast('Refresh', 'Saldo & spin status diupdate', 'success');
      }
    } catch (e) {
      showToast('Refresh Error', e.message || 'Gagal refresh', 'error');
    }
    setTimeout(() => {
      if (btn) { btn.classList.remove('spinning'); btn.disabled = false; }
    }, 600);
  }
  function bindRefreshBtn() {
    const btn = document.getElementById('navRefreshBtn');
    if (btn && btn.dataset.bound !== '1') {
      btn.dataset.bound = '1';
      btn.addEventListener('click', refreshUserData);
    }
  }
  window.refreshUserData = refreshUserData;
  window.bindRefreshBtn = bindRefreshBtn;

  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
  else start();

  // v15: For pages WITHOUT spinBtn (lucky-wheel, collection, beranda subpages)
  if (!document.getElementById('spinBtn')) {
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', bindRefreshBtn);
    } else {
      bindRefreshBtn();
    }
  }
})();
