// CrowdMix — shared helpers : pochettes, icônes, utilitaires API
// Exporté sur window pour être réutilisé par client / display / back-office.

// ── deterministic palette from a string seed ──────────────────
function cmHash(str){let h=0;for(let i=0;i<str.length;i++){h=(h<<5)-h+str.charCodeAt(i);h|=0;}return Math.abs(h);}
function cmCoverColors(seed){
  const h=cmHash(seed);
  const a=h%360, b=(a+40+h%60)%360;
  const sat=42+h%18, lit=22+h%10;
  return [`hsl(${a} ${sat}% ${lit}%)`, `hsl(${b} ${sat-6}% ${Math.max(10,lit-12)}%)`];
}

// ── Résolution adaptative des covers ──────────────────────────
// Qobuz (source actuelle) : URLs static.qobuz.com/images/covers/.../<hash>_<taille>.jpg
//   tailles courantes : _50 · _230 · _600. Simple substitution du suffixe de taille.
//   Petites vignettes (recherche, candidats, listes) → _230 (net même en retina),
//   grands affichages (cover display) → _600. Évite de charger du _600 partout.
// Spotify (legacy, covers non encore re-backfillées) : i.scdn.co — conservé en repli.
function cmArtUrl(src,size){
  if(!src||typeof src!=='string') return src;
  if(src.indexOf('static.qobuz.com')!==-1){
    const target = size<=150 ? '_230' : '_600';
    return src.replace(/_\d+(\.[a-z]+)(\?.*)?$/i, target+'$1$2');
  }
  if(src.indexOf('i.scdn.co/image/')!==-1){
    if(size<=150) return src.replace('ab67616d0000b273','ab67616d00001e02').replace('ab67616d00004851','ab67616d00001e02');
    return src.replace('ab67616d00001e02','ab67616d0000b273').replace('ab67616d00004851','ab67616d0000b273');
  }
  return src;
}

// ── Cover : image réelle ou placeholder déterministe ─────────
function Cover({seed='x', src=null, size=56, radius=10, style={}}){
  if(src){
    return <img src={cmArtUrl(src,size)} alt="" style={{width:size,height:size,borderRadius:radius,objectFit:'cover',flexShrink:0,...style}}/>;
  }
  const [c1,c2]=cmCoverColors(seed);
  const r=size*0.27;
  return (
    <div style={{width:size,height:size,borderRadius:radius,position:'relative',overflow:'hidden',
      background:`linear-gradient(135deg, ${c1}, ${c2})`,flexShrink:0,...style}}>
      <div style={{position:'absolute',inset:0,background:
        `radial-gradient(circle at 32% 28%, rgba(255,255,255,0.16), transparent 46%)`}}/>
      <svg width={size} height={size} viewBox="0 0 100 100" style={{position:'absolute',inset:0,opacity:0.9}}>
        <circle cx="50" cy="50" r={r/size*100} fill="rgba(0,0,0,0.30)"/>
        <circle cx="50" cy="50" r={r/size*100} fill="none" stroke="rgba(255,255,255,0.10)" strokeWidth="1"/>
        <circle cx="50" cy="50" r={r/size*100*0.30} fill="rgba(255,255,255,0.22)"/>
        <circle cx="50" cy="50" r={r/size*100*0.10} fill="rgba(0,0,0,0.4)"/>
      </svg>
    </div>
  );
}

// ── icônes (stroke, hérite de currentColor) ───────────────────
const I = {
  search:(p)=>(<svg viewBox="0 0 24 24" fill="none" {...p}><circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="2"/><path d="M20 20l-3.2-3.2" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/></svg>),
  play:(p)=>(<svg viewBox="0 0 24 24" {...p}><path d="M7 5l12 7-12 7V5z" fill="currentColor"/></svg>),
  pause:(p)=>(<svg viewBox="0 0 24 24" {...p}><rect x="6" y="5" width="4" height="14" rx="1" fill="currentColor"/><rect x="14" y="5" width="4" height="14" rx="1" fill="currentColor"/></svg>),
  check:(p)=>(<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M5 12.5l4.5 4.5L19 7" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"/></svg>),
  close:(p)=>(<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round"/></svg>),
  bolt:(p)=>(<svg viewBox="0 0 24 24" {...p}><path d="M13 2L4 14h6l-1 8 9-12h-6l1-8z" fill="currentColor"/></svg>),
  up:(p)=>(<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M12 19V5M6 11l6-6 6 6" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/></svg>),
  trophy:(p)=>(<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M7 4h10v4a5 5 0 01-10 0V4z" stroke="currentColor" strokeWidth="2" strokeLinejoin="round"/><path d="M7 6H4v1a3 3 0 003 3M17 6h3v1a3 3 0 01-3 3M9 19h6M12 14v5" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/></svg>),
  google:(p)=>(<svg viewBox="0 0 24 24" {...p}><path fill="#4285F4" d="M22.5 12.2c0-.7-.1-1.4-.2-2H12v3.8h5.9a5 5 0 01-2.2 3.3v2.7h3.6c2.1-1.9 3.2-4.8 3.2-7.8z"/><path fill="#34A853" d="M12 23c2.9 0 5.4-1 7.2-2.6l-3.6-2.7c-1 .7-2.3 1.1-3.6 1.1-2.8 0-5.1-1.9-6-4.4H2.3v2.8A11 11 0 0012 23z"/><path fill="#FBBC05" d="M6 14.4a6.6 6.6 0 010-4.2V7.4H2.3a11 11 0 000 9.8L6 14.4z"/><path fill="#EA4335" d="M12 5.5c1.6 0 3 .5 4.1 1.6l3.1-3.1A11 11 0 002.3 7.4L6 10.2c.9-2.5 3.2-4.4 6-4.4z"/></svg>),
  apple:(p)=>(<svg viewBox="0 0 24 24" {...p}><path fill="currentColor" d="M16.4 12.6c0-2.3 1.9-3.4 2-3.5-1.1-1.6-2.8-1.8-3.4-1.8-1.4-.1-2.8.9-3.5.9-.7 0-1.8-.8-3-.8-1.5 0-2.9.9-3.7 2.3-1.6 2.7-.4 6.8 1.1 9 .8 1.1 1.6 2.3 2.8 2.2 1.1 0 1.5-.7 2.9-.7 1.3 0 1.7.7 2.9.7 1.2 0 2-1.1 2.7-2.1.9-1.3 1.2-2.5 1.2-2.6 0 0-2.3-.9-2.3-3.5zM14.2 5.8c.6-.8 1-1.8.9-2.8-.9 0-2 .6-2.6 1.3-.6.7-1.1 1.7-1 2.7 1 .1 2-.5 2.7-1.2z"/></svg>),
  clock:(p)=>(<svg viewBox="0 0 24 24" fill="none" {...p}><circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2"/><path d="M12 7v5l3 2" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/></svg>),
  bell:(p)=>(<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M6 9a6 6 0 1112 0c0 4 1.5 5 2 6H4c.5-1 2-2 2-6z" stroke="currentColor" strokeWidth="2" strokeLinejoin="round"/><path d="M10 20a2 2 0 004 0" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/></svg>),
  chevron:(p)=>(<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M6 9l6 6 6-6" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/></svg>),
  logout:(p)=>(<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M15 4h3a1 1 0 011 1v14a1 1 0 01-1 1h-3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/><path d="M10 8l-4 4 4 4M6 12h10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>),
  plus:(p)=>(<svg viewBox="0 0 24 24" fill="none" {...p}><path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"/></svg>),
};

// ── FlipList : glissement animé des éléments quand leur ordre change ─────────
// Technique FLIP (First-Last-Invert-Play) : on mémorise la position de chaque enfant
// (attribut data-flip) et, quand elle change, on rejoue le glissement de l'ancienne vers
// la nouvelle position.
//
// Deux choix critiques pour le DISPLAY (qui a des particularités que le mobile n'a pas) :
//   1. Mesure via `offsetTop` (PAS getBoundingClientRect) : offsetTop renvoie la position
//      de LAYOUT, insensible (a) à la mise à l'échelle CSS du display (scale-wrap 2560×1080
//      → getBoundingClientRect renverrait des px scalés, incohérents avec le translateY qui
//      s'applique avant le scale) et (b) à un transform/animation en cours sur l'élément
//      (le display se re-rend chaque seconde : barre de progression + timer ; une mesure
//      en plein glissement recalculerait un translateY parasite → mouvements erratiques).
//   2. Animation via la Web Animations API (`el.animate`) plutôt que `style.transition` :
//      indépendante de `el.style.transition`, donc AUCUN conflit avec la transition
//      box-shadow du flash de vote (les deux s'écrasaient mutuellement avant). fill par
//      défaut 'none' → aucun transform résiduel ni couche GPU persistante après la fin.
// `mode` : technique d'animation du glissement.
//   'transform' (défaut, MOBILE) : translateY via Web Animations API — fluide (compositor),
//      offsetTop immunisé contre la mesure en cours.
//   'position' (DISPLAY) : anime la propriété `top` (position:relative) au lieu de `transform`.
//      OBLIGATOIRE sur le display : les cartes vivent dans un conteneur mis à l'échelle
//      (scale-wrap 2560×1080) et animer un `transform` à l'intérieur d'un scale() crée une
//      couche composite que le moteur de rendu (BrightSign en prod, Chrome desktop en test)
//      ne repeint pas correctement → tuiles fantômes (anciennes cartes, timer 00:00 résiduel).
//      `top` est une propriété de layout/paint (pas de couche composite) → repeinte propre.
// `flipKey` : signature de l'ORDRE courant (ex: ids triés joints). Le FLIP n'est mesuré que
//      quand cette clé change → un re-rendu à la seconde (barre de progression / timer) ne
//      déclenche plus de fausse animation.
function FlipList({children,style={},mode='transform',flipKey=''}){
  const ref=React.useRef(null);
  const prevRef=React.useRef(new Map());
  React.useLayoutEffect(()=>{
    const cont=ref.current;if(!cont)return;
    // Sonde perf passive (diagnostic gel display) : coût réel de la passe FLIP
    // (mesures offsetTop + démarrage des animations). Rapportée au hook optionnel
    // window.cmPerfFlip — posé par le display uniquement (no-op partout ailleurs).
    const _t0=(typeof performance!=='undefined')?performance.now():0;
    let _moved=0;
    const next=new Map();
    for(const el of Array.from(cont.children)){
      const k=el.getAttribute('data-flip');if(!k)continue;
      const top=el.offsetTop; // position de layout
      next.set(k,top);
      const old=prevRef.current.get(k);
      if(old!=null){
        const dy=old-top;
        if(Math.abs(dy)>4&&typeof el.animate==='function'){
          _moved++;
          if(el._flipAnim)el._flipAnim.cancel(); // remplace une anim en cours (re-tri rapide)
          if(mode==='position'){
            // DISPLAY : glissement COMPOSITÉ via translate3d (GPU), pas via `top` (paint coûteux
            // → saccades sur BrightSign). Les fantômes d'antan venaient du CHURN de couche
            // (couche créée pour l'anim puis détruite à la fin) : ici les cartes du display ont
            // une couche GPU PERMANENTE (ds.row : will-change:transform + backface-visibility)
            // → la couche ne se détruit jamais → repeinte propre, aucun résidu. fill:none.
            el._flipAnim=el.animate(
              [{transform:`translate3d(0,${dy}px,0)`},{transform:'translate3d(0,0,0)'}],
              {duration:480,easing:'cubic-bezier(.33,1,.68,1)'} // ease-out pur, sans rebond
            );
          }else{
            el._flipAnim=el.animate(
              [{transform:`translateY(${dy}px)`},{transform:'translateY(0)'}],
              {duration:480,easing:'cubic-bezier(.33,1,.68,1)'}
            );
          }
        }
      }
    }
    prevRef.current=next;
    if(_moved>0&&typeof window.cmPerfFlip==='function'){
      try{window.cmPerfFlip(performance.now()-_t0,_moved);}catch(_){}
    }
  },[flipKey]);
  return <div ref={ref} style={style}>{children}</div>;
}

// ── useVoteFlash : flash bref quand le compteur de votes d'une carte augmente ───────
// Retourne un booléen `flash` qui passe à true ~650ms à chaque incrément de `votes`.
// À utiliser pour piloter un box-shadow inline avec transition (PAS une animation CSS
// keyframes ajoutée à chaud — règle anti-fantôme iOS v2.6.6) :
//   flash ? {boxShadow:'…gold…',transition:'box-shadow 0s'} : {transition:'box-shadow .65s ease'}
// → le halo apparaît instantanément puis s'estompe en douceur, pas de promotion de couche.
function useVoteFlash(votes){
  const prev=React.useRef(votes);
  const [flash,setFlash]=React.useState(false);
  React.useEffect(()=>{
    if(votes>prev.current){
      prev.current=votes;
      setFlash(true);
      const t=setTimeout(()=>setFlash(false),950);
      return ()=>clearTimeout(t);
    }
    prev.current=votes;
  },[votes]);
  return flash;
}

// ── Preview Deezer JSONP ─────────────────────────────────────
// Contourne le CORS de api.deezer.com via un <script> tag (JSONP).
// Aucun appel serveur — chaque client résout l'URL directement.
// L'audio dzcdn.net est jouable directement par <audio> sans CORS.
function cmDeezerPreview(q){
  return new Promise((resolve)=>{
    const cb='dz_'+Date.now()+'_'+Math.random().toString(36).slice(2,7);
    const script=document.createElement('script');
    const done=(url)=>{clearTimeout(t);delete window[cb];script.remove();resolve(url);};
    const t=setTimeout(()=>done(null),5000);
    window[cb]=(d)=>done(d?.data?.[0]?.preview||null);
    script.onerror=()=>done(null);
    script.src=`https://api.deezer.com/search?q=${encodeURIComponent(q)}&limit=1&output=jsonp&callback=${cb}`;
    document.head.appendChild(script);
  });
}

Object.assign(window,{Cover, CMI:I, cmCoverColors, cmHash, cmArtUrl, FlipList, useVoteFlash, cmDeezerPreview});
