(() => {
  const likeTotalEl = document.getElementById('likeTotal');
  const gifterListEl = document.querySelector('#topGifters .board-list');
  const likerListEl = document.querySelector('#topLikers .board-list');

  const params = new URLSearchParams(location.search);
  const profileId = params.get('profile') || 'default';

  let profile = null;
  let mediaConfig = null;
  let likeTotal = 0;
  let centerBusy = false;
  const mediaQueue = [];

  // --- Source camera : webcam ou fenetre/ecran choisie dans le navigateur.
  const cameraSlot = document.getElementById('cameraSlot');
  const cameraVideo = document.getElementById('cameraVideo');
  const pickWebcamBtn = document.getElementById('pickWebcam');
  const pickWindowBtn = document.getElementById('pickWindow');

  function stopCurrentStream() {
    const current = cameraVideo.srcObject;
    if (current) current.getTracks().forEach((t) => t.stop());
    cameraVideo.srcObject = null;
    cameraSlot.classList.remove('active');
  }

  function setCameraStream(stream) {
    stopCurrentStream();
    cameraVideo.srcObject = stream;
    cameraSlot.classList.add('active');
    stream.getVideoTracks()[0].addEventListener('ended', () => stopCurrentStream());
  }

  async function pickWebcam() {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
      setCameraStream(stream);
    } catch (e) {
      // permission refusee ou pas de camera
    }
  }

  async function pickWindow() {
    try {
      const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false });
      setCameraStream(stream);
    } catch (e) {
      // annule par l'utilisateur
    }
  }

  if (pickWebcamBtn) pickWebcamBtn.addEventListener('click', pickWebcam);
  if (pickWindowBtn) pickWindowBtn.addEventListener('click', pickWindow);

  // --- Classements top dons / top likes (avec avatars)
  const gifters = new Map(); // uniqueId -> { total, avatar }
  const likers = new Map();

  function bumpLeaderboard(map, uniqueId, amount, avatar) {
    if (!uniqueId || !amount) return;
    const entry = map.get(uniqueId) || { total: 0, avatar: null };
    entry.total += amount;
    if (avatar) entry.avatar = avatar;
    map.set(uniqueId, entry);
  }

  // Une explosion (-1 etage) est declenchee par le jeu tous les 500 likes :
  // la barre montre ou on en est par rapport au prochain palier.
  // Surcharge par public/overlay/game-config.json (meme fichier que le jeu),
  // et mise a jour en direct depuis le tableau de bord.
  let LIKE_MILESTONE = 500;
  function appliquerConfig(cfg) {
    if (!cfg) return;
    const palier = Math.floor(Number(cfg.likeMilestone));
    if (palier > 0) { LIKE_MILESTONE = palier; renderLikeProgress(); }
  }
  fetch('game-config.json').then((r) => r.json()).then(appliquerConfig).catch(() => {});
  TikTokEvents.on('config', appliquerConfig);
  const likeProgressLabelEl = document.getElementById('likeProgressLabel');
  const likeProgressFillEl = document.getElementById('likeProgressFill');

  function renderLikeProgress() {
    const prochainPalier = (Math.floor(likeTotal / LIKE_MILESTONE) + 1) * LIKE_MILESTONE;
    const dansLePalier = likeTotal % LIKE_MILESTONE;
    likeProgressLabelEl.textContent = `${likeTotal}/${prochainPalier} ❤️`;
    likeProgressFillEl.style.width = (dansLePalier / LIKE_MILESTONE) * 100 + '%';
  }

  const MEDALS = ['🥇', '🥈', '🥉'];
  const MIN_BOARD_ROWS = 10; // les 10 places sont toujours affichees (les libres = "-" / 0)

  function renderBoard(listEl, map, opts) {
    opts = opts || {};
    const ranked = [...map.entries()].sort((a, b) => b[1].total - a[1].total).slice(0, 10);
    const lignes = ranked.map(([name, entry], i) => {
      const rank = i + 1;
      const cls = rank <= 3 ? `top${rank}` : '';
      const rankLabel = rank <= 3 ? MEDALS[rank - 1] : String(rank);
      const initial = (name || '?').charAt(0).toUpperCase();
      const avatarInner = entry.avatar
        ? `<img class="avatar" src="${entry.avatar}" alt="" onerror="this.replaceWith(Object.assign(document.createElement('span'),{className:'avatar avatar-fallback',textContent:'${initial}'}))">`
        : `<span class="avatar avatar-fallback">${initial}</span>`;
      // Le tout premier donateur porte une petite couronne au-dessus de sa photo.
      const avatar = (opts.crownTop1 && rank === 1)
        ? `<span class="avatar-wrap"><img class="top-crown" src="media/images/couronne.png" alt="">${avatarInner}</span>`
        : avatarInner;
      return `<div class="board-row ${cls}">${avatar}<span class="rank">${rankLabel}</span><span class="name">${name}</span><span class="value">${entry.total}</span></div>`;
    });

    // Places encore libres : un tiret et un score de 0, pour que le classement
    // ne soit jamais vide et garde toujours la meme hauteur.
    while (lignes.length < MIN_BOARD_ROWS) {
      const rank = lignes.length + 1;
      const rankLabel = rank <= 3 ? MEDALS[rank - 1] : String(rank);
      // Meme classe top1/top2/top3 que pour une vraie personne : la ligne a
      // EXACTEMENT la meme taille avant et apres, pour que le cadrage de
      // l'overlay (crop) ne bouge jamais.
      const cls = rank <= 3 ? `top${rank}` : '';
      lignes.push(
        `<div class="board-row ${cls} board-empty">` +
        `<span class="avatar avatar-vide"></span>` + // place libre : aucune photo
        `<span class="rank">${rankLabel}</span>` +
        `<span class="name">-</span><span class="value">0</span></div>`
      );
    }

    listEl.innerHTML = lignes.join('');
  }

  function renderLeaderboards() {
    renderBoard(gifterListEl, gifters, { crownTop1: true });
    renderBoard(likerListEl, likers);
  }

  // Le panneau etage (Stack) est affiche sur la page du jeu, pas ici.

  function applyProfileTheme() {
    if (!profile) return;
    document.documentElement.style.setProperty('--accent', profile.accent || '#ff2d78');
    document.documentElement.style.setProperty('--accent2', profile.accent2 || '#3fd0ff');
  }

  // --- Legende des dons : grandes cases claires reparties sur les deux colonnes
  // laterales (le centre reste degage pour le jeu). PNG dans media/gifts/ nommes
  // Dons00.png ... Dons12.png ; repli visuel tant que l'image n'existe pas.
  let cellIndex = 0;
  const giftIndex = new Map(); // cle normalisee (id ou nom) -> { cellEl, entry }

  const DIACRITICS_RE = new RegExp('[\\u0300-\\u036f]', 'g');
  function normalizeKey(s) {
    return (s || '').toString().normalize('NFD').replace(DIACRITICS_RE, '').toLowerCase().replace(/[^a-z0-9]/g, '');
  }

  function buildGiftCell(entry) {
    const cell = document.createElement('div');
    cell.className = 'gift-cell';
    cell.dataset.color = entry.color;
    cell.style.setProperty('--i', String(cellIndex++));

    // Chaque case respire a son propre rythme (echelle + flottement), et l'image
    // a l'interieur se balance vite entre -5 et +5 degres, a sa propre cadence.
    cell.style.setProperty('--breathe-dur', (3 + Math.random() * 3.5).toFixed(2) + 's');
    cell.style.setProperty('--breathe-delay', (Math.random() * 2.5).toFixed(2) + 's');
    cell.style.setProperty('--breathe-scale', (1.03 + Math.random() * 0.04).toFixed(3));
    cell.style.setProperty('--swing-dur', (0.7 + Math.random() * 0.8).toFixed(2) + 's');
    cell.style.setProperty('--swing-delay', (Math.random() * 1.2).toFixed(2) + 's');
    cell.style.setProperty('--shine-delay', (Math.random() * 4).toFixed(2) + 's');

    const iconWrap = document.createElement('div');
    iconWrap.className = 'gift-icon-wrap';

    const img = document.createElement('img');
    img.src = entry.image;
    img.alt = entry.id;
    img.onerror = () => {
      img.remove();
      const fallback = document.createElement('div');
      fallback.className = 'gift-fallback';
      fallback.textContent = entry.image.split('/').pop();
      iconWrap.appendChild(fallback);
    };
    iconWrap.appendChild(img);

    const text = document.createElement('div');
    text.className = 'gift-text';

    const name = document.createElement('div');
    name.className = 'gift-name';
    name.textContent = entry.name || entry.id;

    // L'info qui compte (ex: "+1") en gros et en gras, la precision
    // (ex: "etage") en petit a cote : lisible d'un coup d'oeil par les viewers.
    const caption = document.createElement('div');
    caption.className = 'gift-caption';
    if (entry.effet) {
      const effet = document.createElement('span');
      // Un libelle long ("Vitesse x2") ne peut pas etre aussi gros qu'un
      // court ("+1") sans rogner le detail a cote : on l'ecrit un peu plus
      // petit, tout en restant nettement plus gros que le detail.
      effet.className = 'gift-effet' + (String(entry.effet).length > 6 ? ' gift-effet-long' : '');
      effet.textContent = entry.effet;
      caption.appendChild(effet);
      if (entry.detail) {
        const detail = document.createElement('span');
        detail.className = 'gift-detail';
        detail.textContent = entry.detail;
        caption.appendChild(detail);
      }
    } else {
      caption.textContent = entry.caption || '';
    }

    text.appendChild(name);
    text.appendChild(caption);

    cell.appendChild(iconWrap);
    cell.appendChild(text);

    giftIndex.set(normalizeKey(entry.id), { cellEl: cell, entry });
    giftIndex.set(normalizeKey(entry.name), { cellEl: cell, entry });
    // Noms anglais reellement envoyes par TikTok (voir "alias" dans le JSON).
    for (const a of entry.alias || []) giftIndex.set(normalizeKey(a), { cellEl: cell, entry });

    return cell;
  }

  function renderGiftLegend(legend, prices) {
    const left = document.getElementById('sideLeft');
    const right = document.getElementById('sideRight');

    // gift-prices.json surcharge le prix de reference de gift-legend.json,
    // pour pouvoir corriger un prix sans toucher au reste de la legende.
    const all = [...legend.grid, ...legend.sideLeft, ...legend.sideRight];
    if (prices) {
      [...all, legend.special].forEach((entry) => {
        if (prices[entry.id] !== undefined) entry.diamonds = prices[entry.id];
      });
    }
    // 2 colonnes x 3 lignes de rouges a gauche, autant de verts a droite.
    all.filter((e) => e.color === 'red').forEach((entry) => left.appendChild(buildGiftCell(entry)));
    all.filter((e) => e.color !== 'red').forEach((entry) => right.appendChild(buildGiftCell(entry)));

    // Le don special (bleu) est seul au centre, entre les deux grilles.
    const special = buildGiftCell(legend.special);
    special.classList.add('gift-cell-special');
    document.getElementById('giftSpecial').appendChild(special);
  }

  // --- Reaction visuelle quand un don arrive : la case s'agrandit/retrecit vite
  // (plus longtemps pour les dons chers), et le pseudo du donateur apparait
  // au-dessus de l'icone puis disparait, pendant la duree propre au don.
  const labelLayer = document.createElement('div');
  labelLayer.id = 'giftLabelLayer';
  document.body.appendChild(labelLayer);

  const activeLabels = new Map(); // cellEl -> { el, timer }
  const POP_BASE_MS = 380;
  const DONOR_LABEL_MS = 15000; // le pseudo reste affiche 15s, pareil pour tous les dons
  const LABEL_FAST_MS = 260; // vitesse fixe d'entree/sortie du pseudo, quelle que soit la duree totale

  function popCell(cellEl, multiplier) {
    const dur = Math.min(POP_BASE_MS * (multiplier || 1), 30000);
    cellEl.animate([
      { transform: 'scale(1)' },
      { transform: 'scale(1.38)', offset: 0.3 },
      { transform: 'scale(0.94)', offset: 0.62 },
      { transform: 'scale(1)' }
    ], { duration: dur, easing: 'cubic-bezier(.34,1.56,.64,1)' });
  }

  function showDonorLabel(cellEl, uniqueId, durationMs) {
    const prev = activeLabels.get(cellEl);
    if (prev) { clearTimeout(prev.timer); prev.el.remove(); }

    const iconRect = cellEl.querySelector('.gift-icon-wrap').getBoundingClientRect();
    const label = document.createElement('div');
    label.className = 'gift-donor-label';
    label.textContent = uniqueId || '???';
    label.style.left = (iconRect.left + iconRect.width / 2) + 'px';
    label.style.top = iconRect.top + 'px';
    labelLayer.appendChild(label);

    // entree/sortie toujours rapides (LABEL_FAST_MS), le pseudo reste affiche
    // entre les deux pendant tout le reste de durationMs.
    const inOffset = Math.min(0.4, LABEL_FAST_MS / durationMs);
    const outOffset = Math.max(0.6, 1 - LABEL_FAST_MS / durationMs);

    // translate(-50%, -100% - Npx) : la pastille reste toujours entierement
    // au-dessus de l'icone (-100% = sa propre hauteur), avec un petit flottement.
    label.animate([
      { opacity: 0, transform: 'translate(-50%, calc(-100% + 6px)) scale(0.6)', offset: 0 },
      { opacity: 1, transform: 'translate(-50%, calc(-100% - 10px)) scale(1.08)', offset: inOffset },
      { opacity: 1, transform: 'translate(-50%, calc(-100% - 10px)) scale(1)', offset: Math.min(inOffset + 0.03, outOffset) },
      { opacity: 1, transform: 'translate(-50%, calc(-100% - 10px)) scale(1)', offset: outOffset },
      { opacity: 0, transform: 'translate(-50%, calc(-100% - 20px)) scale(0.6)', offset: 1 }
    ], { duration: durationMs, easing: 'ease-out', fill: 'forwards' });

    const timer = setTimeout(() => { label.remove(); activeLabels.delete(cellEl); }, durationMs + 50);
    activeLabels.set(cellEl, { el: label, timer });
  }

  // --- Effet a l'echelle de l'ecran (a partir de Galaxie), puissance proportionnelle
  // au prix reel du don (diamants). Confettis colores + flash + petit tremblement.
  function spawnScreenEffect(power, hue) {
    // (pas de confettis/particules : uniquement le flash colore + le tremblement)
    const flash = document.createElement('div');
    flash.style.position = 'fixed';
    flash.style.inset = '0';
    flash.style.background = `hsla(${hue}, 90%, 70%, ${Math.min(0.35, 0.08 * power)})`;
    flash.style.zIndex = '996';
    flash.style.pointerEvents = 'none';
    document.body.appendChild(flash);
    const flashAnim = flash.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 500, easing: 'ease-out' });
    flashAnim.onfinish = () => flash.remove();

    if (document.body.animate) {
      const shakeAmt = Math.min(14, 3 * power);
      document.body.animate([
        { transform: 'translate(0,0)' },
        { transform: `translate(${shakeAmt}px, ${-shakeAmt}px)` },
        { transform: `translate(${-shakeAmt}px, ${shakeAmt * 0.6}px)` },
        { transform: 'translate(0,0)' }
      ], { duration: 260, easing: 'ease-in-out' });
    }
  }

  function screenEffectPower(diamonds) {
    return Math.max(1, Math.min(60, diamonds / 20));
  }

  const specialIntervals = new Map();

  function reactToGift(giftName, uniqueId, realDiamonds, repeatCount) {
    const key = normalizeKey(giftName);
    const found = giftIndex.get(key);

    if (!found) {
      console.warn(`[overlay] Don inconnu : "${giftName}" - ajoute-le dans gift-legend.json si besoin.`);
      return;
    }

    const { cellEl, entry } = found;
    const diamonds = (realDiamonds || entry.diamonds) * (repeatCount || 1);

    popCell(cellEl, entry.effectMultiplier);
    // Le pseudo reste affiche 15s pour TOUS les dons (uniforme, quel que soit le palier).
    showDonorLabel(cellEl, uniqueId, DONOR_LABEL_MS);

    if (entry.screenEffect) {
      const hue = cellEl.dataset.color === 'red' ? 350 : cellEl.dataset.color === 'blue' ? 195 : 140;
      spawnScreenEffect(screenEffectPower(diamonds), hue);

      // Le don special (seul, tout en haut de gamme) enchaine plusieurs effets
      // sur toute sa duree d'affichage, pas juste un seul coup.
      if (entry.screenEffectRepeatMs) {
        const prevInterval = specialIntervals.get(entry.id);
        if (prevInterval) clearInterval(prevInterval);
        const interval = setInterval(() => spawnScreenEffect(screenEffectPower(diamonds), hue), entry.screenEffectRepeatMs);
        specialIntervals.set(entry.id, interval);
        setTimeout(() => { clearInterval(interval); specialIntervals.delete(entry.id); }, entry.durationMs);
      }
    }
  }

  // --- Media plein-ecran programmable (video/son au centre), via media-config.json.
  function enqueueCenterMedia(cfg) {
    if (!cfg || (!cfg.video && !cfg.sound)) return;
    mediaQueue.push(cfg);
    processMediaQueue();
  }

  function processMediaQueue() {
    if (centerBusy || mediaQueue.length === 0) return;
    centerBusy = true;
    const cfg = mediaQueue.shift();
    playCenterMedia(cfg, () => {
      centerBusy = false;
      processMediaQueue();
    });
  }

  function playCenterMedia(cfg, done) {
    const wrap = document.createElement('div');
    wrap.style.position = 'fixed';
    wrap.style.inset = '0';
    wrap.style.display = 'flex';
    wrap.style.alignItems = 'center';
    wrap.style.justifyContent = 'center';
    wrap.style.pointerEvents = 'none';
    wrap.style.zIndex = '999';

    let finished = false;
    const finish = () => {
      if (finished) return;
      finished = true;
      wrap.remove();
      done();
    };

    let mediaEl = null;
    if (cfg.video) {
      mediaEl = document.createElement('video');
      mediaEl.src = cfg.video;
      mediaEl.style.maxWidth = '70vw';
      mediaEl.style.maxHeight = '50vh';
      mediaEl.style.borderRadius = '24px';
      mediaEl.style.boxShadow = '0 20px 60px rgba(0,0,0,0.5)';
      mediaEl.autoplay = true;
      mediaEl.addEventListener('ended', finish);
      mediaEl.addEventListener('error', finish);
      wrap.appendChild(mediaEl);
    }

    if (cfg.sound) {
      const audio = new Audio(cfg.sound);
      audio.play().catch(() => {});
      if (!cfg.video) audio.addEventListener('ended', finish);
      audio.addEventListener('error', () => { if (!cfg.video) finish(); });
    }

    document.body.appendChild(wrap);

    const fallbackDuration = cfg.durationMs || 4000;
    setTimeout(finish, fallbackDuration + 300);

    if (mediaEl) {
      mediaEl.play().catch(() => {
        mediaEl.muted = true;
        mediaEl.play().catch(finish);
      });
    }
  }

  function mediaForGift(giftName) {
    if (!mediaConfig || !mediaConfig.gift) return null;
    const key = (giftName || '').toLowerCase();
    return mediaConfig.gift[key] || mediaConfig.gift.default || null;
  }

  // --- Evenements TikTok
  function onLike(data) {
    const taps = Number(data.likeCount) > 0 ? Number(data.likeCount) : 1;
    // likesTotal = total du live tel que TikTok le compte (donc le meme
    // chiffre que celui affiche sur le live), partage avec le jeu.
    const total = Number(data.likesTotal) || Number(data.likesSession);
    likeTotal = total > 0 ? total : likeTotal + taps;
    likeTotalEl.textContent = String(likeTotal);
    // `likeCount` = nombre de tapotements de CET evenement (pas un cumul) :
    // on additionne, sinon renvoyer deux fois la meme valeur ne bouge plus rien.
    bumpLeaderboard(likers, data.uniqueId, taps, data.profilePictureUrl);
    renderLikeProgress();
    renderLeaderboards();

    if (mediaConfig && mediaConfig.likeMilestone && mediaConfig.likeMilestone.every) {
      const every = mediaConfig.likeMilestone.every;
      if (Math.floor(likeTotal / every) > Math.floor((likeTotal - (data.likeCount || 1)) / every)) {
        enqueueCenterMedia(mediaConfig.likeMilestone);
      }
    }
  }

  function onGift(data) {
    const diamonds = (data.diamondCount || 0) * (data.repeatCount || 1);
    bumpLeaderboard(gifters, data.uniqueId, diamonds, data.profilePictureUrl);
    renderLeaderboards();
    reactToGift(data.giftName, data.uniqueId, data.diamondCount, data.repeatCount);
    enqueueCenterMedia(mediaForGift(data.giftName));
  }

  // --- Alerte follow : une pastille bien visible apparait puis disparait,
  // independamment des cases de dons (les follows n'en ont pas).
  // Place juste apres la camera dans le flux (hauteur nulle) : l'alerte tombe
  // toujours pile sous la camera, sans calcul de position a refaire au resize.
  const followAlertLayer = document.createElement('div');
  followAlertLayer.id = 'followAlertLayer';
  cameraSlot.insertAdjacentElement('afterend', followAlertLayer);

  function showFollowAlert(uniqueId) {
    const card = document.createElement('div');
    card.className = 'follow-alert';
    card.innerHTML = `<span class="follow-alert-icon">⭐</span><span>${uniqueId || '???'} vient de follow !</span>`;
    followAlertLayer.appendChild(card);
    card.animate([
      { opacity: 0, transform: 'translate(-50%, 16px) scale(0.7)', offset: 0 },
      { opacity: 1, transform: 'translate(-50%, 0) scale(1.06)', offset: 0.15 },
      { opacity: 1, transform: 'translate(-50%, 0) scale(1)', offset: 0.25 },
      { opacity: 1, transform: 'translate(-50%, 0) scale(1)', offset: 0.8 },
      { opacity: 0, transform: 'translate(-50%, -14px) scale(0.85)', offset: 1 }
    ], { duration: 3200, easing: 'ease-out' });
    setTimeout(() => card.remove(), 3250);
  }

  // Son du follow : joue tout de suite, hors de la file d'attente, pour que
  // deux follows rapproches se superposent (l'un par-dessus l'autre) au lieu
  // de s'enchainer. C'est aussi le SEUL endroit ou ce son est joue.
  let followSoundsPlaying = 0;

  function playFollowSound() {
    const cfg = mediaConfig && mediaConfig.follow;
    if (!cfg || !cfg.sound) return;
    // si un son de follow tourne deja, on en lance deux d'un coup pour que ca
    // s'entende bien par-dessus, sinon un seul.
    const layers = followSoundsPlaying > 0 ? 2 : 1;
    for (let i = 0; i < layers; i++) {
      try {
        const audio = new Audio(cfg.sound);
        followSoundsPlaying++;
        const done = () => { followSoundsPlaying = Math.max(0, followSoundsPlaying - 1); };
        audio.addEventListener('ended', done);
        audio.addEventListener('error', done);
        audio.play().catch(done);
      } catch (e) {
        // audio indisponible
      }
    }
  }

  function onFollow(data) {
    showFollowAlert(data.uniqueId);
    playFollowSound();
  }

  // --- Sons/videos declenches par un mot-cle dans un commentaire (media-config.json).
  function onComment(data) {
    if (!mediaConfig || !Array.isArray(mediaConfig.comments) || !data.comment) return;
    const text = data.comment.toLowerCase();
    const match = mediaConfig.comments.find((entry) =>
      Array.isArray(entry.keywords) && entry.keywords.some((kw) => text.includes(String(kw).toLowerCase()))
    );
    if (match) enqueueCenterMedia(match);
  }

  Promise.all([
    fetch('profiles.json').then((r) => r.json()),
    fetch('media-config.json').then((r) => r.json()).catch(() => null),
    fetch('gift-legend.json').then((r) => r.json()).catch(() => null),
    fetch('gift-prices.json').then((r) => r.json()).catch(() => null)
  ]).then(([profiles, media, legend, prices]) => {
    profile = profiles[profileId] || profiles.default;
    mediaConfig = media;
    applyProfileTheme();
    if (legend) renderGiftLegend(legend, prices);
    renderLikeProgress();
    // Indispensable : sans ca les classements restent VIDES tant que personne
    // n'a like ou donne (les 10 places en tirets n'apparaissaient jamais).
    renderLeaderboards();

    TikTokEvents.on('like', onLike);
    TikTokEvents.on('gift', onGift);
    TikTokEvents.on('follow', onFollow);
    TikTokEvents.on('comment', onComment);
  });
})();
