// playlist.jsx — DJ Antønic Intelligence Hub
const { useState, useEffect, useMemo, useCallback, useRef } = React;
const Eng = window.DJEngine;

// ── Helpers ───────────────────────────────────────────────────────────────────
function uid()       { return Math.random().toString(36).slice(2) + Date.now().toString(36); }
function subGenre(g) { return g?.includes('---') ? g.split('---')[1] : g; }
function fmtDate(ts) { return new Date(ts).toLocaleDateString('es-ES', { day:'2-digit', month:'short', year:'numeric' }); }

function camelotCompatible(key) {
  if (!key) return [];
  const m = key.match(/^(\d+)([AB])$/);
  if (!m) return [key];
  const n = parseInt(m[1]), L = m[2];
  return [key, `${n===1?12:n-1}${L}`, `${n===12?1:n+1}${L}`, `${n}${L==='A'?'B':'A'}`];
}

const ROLE_LABELS = { opener:'Opener', builder:'Builder', peak:'Peak', bridge:'Puente', closer:'Closer' };
const ROLE_COLORS = { opener:'#5cc7ff', builder:'#96f582', peak:'#eb4baf', bridge:'#a86eff', closer:'#f5a623' };
function roleColor(r) { return ROLE_COLORS[r] || 'var(--text-3)'; }
function scoreColor(n) {
  if (n >= 80) return '#96f582';
  if (n >= 60) return '#5cc7ff';
  if (n >= 40) return '#f5a623';
  return 'var(--text-3)';
}

// ── Smart playlist logic ──────────────────────────────────────────────────────
function matchCondition(track, cond) {
  const { field, op, value } = cond;
  if (field === 'bpm') {
    if (!track.bpm) return false;
    if (op === 'between') return track.bpm >= value[0] && track.bpm <= value[1];
    if (op === 'gte')     return track.bpm >= value;
    if (op === 'lte')     return track.bpm <= value;
    if (op === 'eq')      return track.bpm === value;
  }
  if (field === 'camelot') {
    if (!track.camelot) return false;
    if (op === 'compatible') return camelotCompatible(value).includes(track.camelot);
    if (op === 'is')         return track.camelot === value;
  }
  if (field === 'genre') {
    if (!track.genres?.length) return false;
    const lv = (value||'').toLowerCase();
    if (!lv) return true;
    return track.genres.some(g => subGenre(g).toLowerCase().includes(lv) || g.toLowerCase().includes(lv));
  }
  if (field === 'danceability') {
    if (track.danceability == null) return false;
    const pct = track.danceability * 100;
    if (op === 'between') return pct >= value[0] && pct <= value[1];
    if (op === 'gte')     return pct >= value;
    if (op === 'lte')     return pct <= value;
  }
  if (field === 'vocal') {
    if (track.instrumental == null) return false;
    return value === 'instrumental' ? track.instrumental >= 0.5 : track.instrumental < 0.5;
  }
  if (field.startsWith('mood_')) {
    const mk = field.replace('mood_', '');
    const mv = track.moods?.[mk];
    if (mv == null) return false;
    const pct = mv * 100;
    if (op === 'between') return pct >= value[0] && pct <= value[1];
    if (op === 'gte')     return pct >= value;
    if (op === 'lte')     return pct <= value;
  }
  return false;
}

function matchPlaylist(track, pl) {
  if (!pl.conditions?.length) return true;
  if (pl.operator === 'AND') return pl.conditions.every(c => matchCondition(track, c));
  return pl.conditions.some(c => matchCondition(track, c));
}

// ── Exports ───────────────────────────────────────────────────────────────────
function exportM3U(tracks, name) {
  let txt = '#EXTM3U\n';
  txt += '# Generado por DJ Antønic · Pon los archivos en la misma carpeta que este .m3u\n';
  for (const t of tracks) {
    const dur  = t.sections?.duration ?? -1;
    const info = [t.bpm && `${t.bpm} BPM`, t.camelot].filter(Boolean).join(' · ');
    txt += `#EXTINF:${dur},${t.name}${info ? ' — ' + info : ''}\n`;
    txt += (t.filename || `${t.name}.${t.format || 'mp3'}`) + '\n';
  }
  const a = Object.assign(document.createElement('a'), {
    href: URL.createObjectURL(new Blob([txt], { type:'audio/mpegurl' })),
    download: (name||'playlist').replace(/[^\w\s-]/g,'').trim()+'.m3u',
  });
  document.body.appendChild(a); a.click();
  setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(a.href); }, 300);
}

function doExportCSV(tracks) {
  if (!Eng) return;
  const csv = Eng.exportCSV(tracks);
  const a = Object.assign(document.createElement('a'), {
    href: URL.createObjectURL(new Blob([csv], { type:'text/csv;charset=utf-8' })),
    download: 'antonic-biblioteca.csv',
  });
  document.body.appendChild(a); a.click();
  setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(a.href); }, 300);
}

function doExportJSON(tracks) {
  const a = Object.assign(document.createElement('a'), {
    href: URL.createObjectURL(new Blob([JSON.stringify(tracks, null, 2)], { type:'application/json' })),
    download: 'antonic-biblioteca.json',
  });
  document.body.appendChild(a); a.click();
  setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(a.href); }, 300);
}

// ── TrackPills ────────────────────────────────────────────────────────────────
function TrackPills({ track }) {
  return (
    <div className="pl-pills">
      {track.bpm     && <span className="fc-pill">{track.bpm} BPM</span>}
      {track.camelot && <span className="fc-pill">{track.camelot}</span>}
      {track.genres?.slice(0,2).map((g,i) => <span key={i} className="fc-pill fc-pill-genre">{subGenre(g)}</span>)}
      {track.danceability != null && <span className="fc-pill">💃 {Math.round(track.danceability*100)}%</span>}
      {track.instrumental != null && (
        <span className="fc-pill fc-pill-voice">{track.instrumental>0.5?'Instrumental':'Vocal'}</span>
      )}
    </div>
  );
}

// ── Track DNA modal ───────────────────────────────────────────────────────────
function TrackDNA({ track, allTracks, onClose }) {
  const [mode, setMode] = useState('safe');
  const [bridgeTarget, setBridgeTarget] = useState(null);

  const score    = Eng ? Eng.getAntonicScore(track) : null;
  const roles    = Eng ? Eng.getDjRoles(track) : null;
  const nextBest = Eng ? Eng.getNextBestTracks(track, allTracks, mode) : [];
  const bridges  = Eng && bridgeTarget ? Eng.findBridgeTracks(track, bridgeTarget, allTracks) : null;
  const risk     = bridgeTarget && Eng ? Eng.analyzeTransitionRisk(track, bridgeTarget) : null;

  const MODES = [
    { id:'safe',        label:'Seguro'   },
    { id:'energy_up',   label:'Subir ↑' },
    { id:'energy_down', label:'Bajar ↓' },
    { id:'bridge',      label:'Puente'   },
  ];

  return (
    <div style={{
      position:'fixed', inset:0, zIndex:200,
      background:'rgba(6,7,11,0.88)', backdropFilter:'blur(10px)',
      display:'flex', alignItems:'flex-start', justifyContent:'center',
      padding:'24px 16px', overflowY:'auto',
    }} onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div style={{
        background:'var(--ink-2)', border:'1px solid var(--line-2)',
        borderRadius:16, padding:28, maxWidth:640, width:'100%',
        boxShadow:'0 24px 60px rgba(0,0,0,0.7)',
      }}>
        <div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom:20 }}>
          <div>
            <div style={{ fontFamily:'var(--font-mono)', fontSize:10, letterSpacing:'.14em', color:'var(--text-3)', textTransform:'uppercase', marginBottom:6 }}>Track DNA</div>
            <h2 style={{ margin:0, fontSize:18, fontWeight:600 }}>{track.name}</h2>
          </div>
          <button onClick={onClose} style={{ background:'none', border:'1px solid var(--line-2)', borderRadius:8, color:'var(--text-3)', padding:'6px 10px', cursor:'pointer', fontSize:13 }}>Cerrar</button>
        </div>

        <div style={{ display:'flex', gap:16, alignItems:'center', marginBottom:20, flexWrap:'wrap' }}>
          {score != null && (
            <div style={{ textAlign:'center', minWidth:70 }}>
              <div style={{ fontSize:36, fontWeight:700, color:scoreColor(score), fontFamily:'var(--font-mono)', lineHeight:1 }}>{score}</div>
              <div style={{ fontSize:10, letterSpacing:'.1em', color:'var(--text-3)', textTransform:'uppercase', marginTop:3 }}>Antønic Score</div>
            </div>
          )}
          <div style={{ flex:1 }}>
            <TrackPills track={track} />
            {roles && (
              <div style={{ marginTop:8, display:'flex', gap:6, flexWrap:'wrap' }}>
                <span style={{ fontFamily:'var(--font-mono)', fontSize:10, letterSpacing:'.12em', color:'var(--text-3)', textTransform:'uppercase', alignSelf:'center' }}>Rol principal:</span>
                <span style={{ background:roleColor(roles.primaryRole)+'22', color:roleColor(roles.primaryRole), border:`1px solid ${roleColor(roles.primaryRole)}44`, borderRadius:6, padding:'3px 8px', fontSize:12, fontWeight:600 }}>
                  {ROLE_LABELS[roles.primaryRole] || roles.primaryRole}
                </span>
              </div>
            )}
          </div>
        </div>

        {roles && (
          <div style={{ marginBottom:22 }}>
            <div style={{ fontFamily:'var(--font-mono)', fontSize:10, letterSpacing:'.12em', color:'var(--text-3)', textTransform:'uppercase', marginBottom:10 }}>Scores de rol</div>
            {Object.entries(roles.scores).map(([role, val]) => (
              <div key={role} style={{ display:'flex', alignItems:'center', gap:10, marginBottom:6 }}>
                <span style={{ fontFamily:'var(--font-mono)', fontSize:11, color:'var(--text-2)', width:56 }}>{ROLE_LABELS[role]}</span>
                <div style={{ flex:1, height:6, background:'var(--ink-4)', borderRadius:3 }}>
                  <div style={{ width:`${val}%`, height:'100%', background:roleColor(role), borderRadius:3, transition:'width .3s' }} />
                </div>
                <span style={{ fontFamily:'var(--font-mono)', fontSize:11, color:roleColor(role), width:26, textAlign:'right' }}>{val}</span>
              </div>
            ))}
          </div>
        )}

        {track.sections && (
          <div style={{ marginBottom:22 }}>
            <div style={{ fontFamily:'var(--font-mono)', fontSize:10, letterSpacing:'.12em', color:'var(--text-3)', textTransform:'uppercase', marginBottom:10 }}>
              Análisis de secciones{track.sections.duration ? ` · ${track.sections.duration}s` : ''}
            </div>
            {[
              { label:'Intro', energy:track.sections.introEnergy, clean:track.sections.introCleanliness },
              { label:'Core',  energy:track.sections.coreEnergy },
              { label:'Outro', energy:track.sections.outroEnergy, clean:track.sections.outroCleanliness },
            ].map(({ label, energy, clean }) => (
              <div key={label} style={{ display:'flex', alignItems:'center', gap:10, marginBottom:6 }}>
                <span style={{ fontFamily:'var(--font-mono)', fontSize:11, color:'var(--text-2)', width:38 }}>{label}</span>
                <div style={{ flex:1, height:5, background:'var(--ink-4)', borderRadius:3 }}>
                  <div style={{ width:`${Math.min(100, energy * 80)}%`, height:'100%', background:energy < 0.7 ? '#5cc7ff' : '#96f582', borderRadius:3 }} />
                </div>
                <span style={{ fontFamily:'var(--font-mono)', fontSize:10, color:'var(--text-3)', width:32, textAlign:'right' }}>{Math.round(energy * 100)}%</span>
                {clean && <span style={{ fontSize:9, color:'#96f582', fontFamily:'var(--font-mono)', width:24 }}>MIX</span>}
              </div>
            ))}
          </div>
        )}

        {allTracks.length > 1 && (
          <div style={{ marginBottom:20 }}>
            <div style={{ display:'flex', gap:6, alignItems:'center', marginBottom:10, flexWrap:'wrap' }}>
              <span style={{ fontFamily:'var(--font-mono)', fontSize:10, letterSpacing:'.12em', color:'var(--text-3)', textTransform:'uppercase' }}>Siguiente mejor pista</span>
              <div style={{ display:'flex', gap:4 }}>
                {MODES.map(m => (
                  <button key={m.id} onClick={() => setMode(m.id)} style={{
                    padding:'3px 8px', borderRadius:5, fontSize:11, border:'1px solid',
                    borderColor: mode===m.id ? 'var(--accent)' : 'var(--line-2)',
                    background: mode===m.id ? 'var(--accent)22' : 'transparent',
                    color: mode===m.id ? 'var(--accent)' : 'var(--text-3)',
                    cursor:'pointer', fontFamily:'var(--font-mono)',
                  }}>{m.label}</button>
                ))}
              </div>
            </div>
            {nextBest.length === 0 ? (
              <div style={{ color:'var(--text-3)', fontSize:13, padding:'10px 0' }}>Analiza más canciones para ver compatibilidades.</div>
            ) : (
              nextBest.slice(0,5).map(({ track:t, matchScore, finalScore, reason, risk:rsk }) => (
                <div key={t.id} style={{
                  display:'flex', alignItems:'center', gap:10, padding:'8px 10px',
                  background:'var(--ink-3)', borderRadius:8, marginBottom:4, border:'1px solid var(--line)',
                }}>
                  <div style={{ fontFamily:'var(--font-mono)', fontSize:13, fontWeight:600, color:scoreColor(finalScore), width:28, textAlign:'center' }}>{finalScore}</div>
                  <div style={{ flex:1, minWidth:0 }}>
                    <div style={{ fontSize:13, fontWeight:500, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{t.name}</div>
                    <div style={{ fontSize:11, color:'var(--text-3)', marginTop:2 }}>{reason} · Riesgo: {rsk}</div>
                  </div>
                  <div style={{ display:'flex', gap:4, flexShrink:0, alignItems:'center' }}>
                    {t.bpm    && <span style={{ fontFamily:'var(--font-mono)', fontSize:10, color:'var(--text-3)' }}>{t.bpm}</span>}
                    {t.camelot && <span style={{ fontFamily:'var(--font-mono)', fontSize:10, color:'var(--text-3)' }}>{t.camelot}</span>}
                    <button title="Buena mezcla" onClick={() => window.LibraryStore?.recordInteraction('accept', track.id, t.id, { bpmScore:Eng?.getBpmCompatibility(track.bpm,t.bpm)??0, camScore:Eng?.getCamelotCompatibility(track.camelot,t.camelot)??0 })} style={{ padding:'2px 5px', borderRadius:4, fontSize:12, border:'1px solid var(--line-2)', background:'transparent', cursor:'pointer' }}>👍</button>
                    <button title="Mala mezcla"  onClick={() => window.LibraryStore?.recordInteraction('reject',  track.id, t.id, { bpmScore:Eng?.getBpmCompatibility(track.bpm,t.bpm)??0, camScore:Eng?.getCamelotCompatibility(track.camelot,t.camelot)??0 })} style={{ padding:'2px 5px', borderRadius:4, fontSize:12, border:'1px solid var(--line-2)', background:'transparent', cursor:'pointer' }}>👎</button>
                    <button onClick={() => setBridgeTarget(t)} style={{ padding:'2px 7px', borderRadius:5, fontSize:10, border:'1px solid var(--line-2)', background:'transparent', color:'var(--text-3)', cursor:'pointer', fontFamily:'var(--font-mono)' }}>puente →</button>
                  </div>
                </div>
              ))
            )}
          </div>
        )}

        {bridgeTarget && bridges && (
          <div>
            <div style={{ fontFamily:'var(--font-mono)', fontSize:10, letterSpacing:'.12em', color:'var(--text-3)', textTransform:'uppercase', marginBottom:8 }}>
              Puentes: {track.name} → <span style={{ color:'var(--accent)' }}>{bridgeTarget.name}</span>
              {risk && <span style={{ marginLeft:8, color:risk.riskScore>60?'#eb4baf':risk.riskScore>30?'#f5a623':'#96f582' }}>(riesgo directo: {risk.riskScore})</span>}
            </div>
            {bridges.length === 0 ? (
              <div style={{ color:'var(--text-3)', fontSize:13 }}>Sin puentes disponibles con los datos actuales.</div>
            ) : (
              bridges.map(({ track:t, score:s, explanation }) => (
                <div key={t.id} style={{ display:'flex', alignItems:'center', gap:10, padding:'8px 10px', background:'var(--ink-3)', borderRadius:8, marginBottom:4, border:'1px solid var(--line)' }}>
                  <div style={{ fontFamily:'var(--font-mono)', fontSize:13, fontWeight:600, color:scoreColor(s), width:28, textAlign:'center' }}>{s}</div>
                  <div style={{ flex:1, minWidth:0 }}>
                    <div style={{ fontSize:13, fontWeight:500 }}>{t.name}</div>
                    <div style={{ fontSize:11, color:'var(--text-3)', marginTop:2 }}>{explanation}</div>
                  </div>
                </div>
              ))
            )}
            <button onClick={() => setBridgeTarget(null)} style={{ marginTop:6, background:'none', border:'none', color:'var(--text-3)', fontSize:12, cursor:'pointer', padding:0, fontFamily:'var(--font-mono)' }}>← Cerrar Bridge Finder</button>
          </div>
        )}
      </div>
    </div>
  );
}

// ── Playlist editor ───────────────────────────────────────────────────────────
const FIELDS = [
  { id:'bpm',          label:'BPM',         type:'numeric' },
  { id:'camelot',      label:'Camelot',      type:'camelot' },
  { id:'genre',        label:'Género',        type:'text' },
  { id:'danceability', label:'Bailabilidad',  type:'pct' },
  { id:'vocal',        label:'Vocal / Inst',  type:'select', options:[{v:'vocal',l:'Vocal'},{v:'instrumental',l:'Instrumental'}] },
  { id:'mood_party',      label:'Mood · Party',    type:'pct' },
  { id:'mood_happy',      label:'Mood · Happy',    type:'pct' },
  { id:'mood_aggressive', label:'Mood · Agresivo', type:'pct' },
  { id:'mood_relaxed',    label:'Mood · Relajado', type:'pct' },
];
const OPS = {
  numeric:[{id:'between',l:'entre'},{id:'gte',l:'≥'},{id:'lte',l:'≤'},{id:'eq',l:'='}],
  camelot:[{id:'compatible',l:'compatible con'},{id:'is',l:'exactamente'}],
  text:   [{id:'contains',l:'contiene'}],
  pct:    [{id:'between',l:'entre'},{id:'gte',l:'≥'},{id:'lte',l:'≤'}],
  select: [{id:'is',l:'es'}],
};
const CAMELOT_KEYS   = ['1A','2A','3A','4A','5A','6A','7A','8A','9A','10A','11A','12A','1B','2B','3B','4B','5B','6B','7B','8B','9B','10B','11B','12B'];
const CAMELOT_LABELS = {'1A':'Am','2A':'Em','3A':'Bm','4A':'F#m','5A':'C#m','6A':'G#m','7A':'Ebm','8A':'Bbm','9A':'Fm','10A':'Cm','11A':'Gm','12A':'Dm','1B':'C','2B':'G','3B':'D','4B':'A','5B':'E','6B':'B','7B':'F#','8B':'Db','9B':'Ab','10B':'Eb','11B':'Bb','12B':'F'};

function defaultVal(fieldId, op) {
  if (op==='between')    return fieldId==='bpm' ? [120,140] : [60,90];
  if (op==='compatible') return '8A';
  if (op==='is')         return FIELDS.find(f=>f.id===fieldId)?.options?.[0]?.v ?? '8A';
  if (op==='contains')   return '';
  return fieldId==='bpm' ? 120 : 70;
}
function makeCondition(fieldId='bpm') {
  const fd = FIELDS.find(f=>f.id===fieldId) || FIELDS[0];
  const op = OPS[fd.type]?.[0]?.id || 'gte';
  return { id:uid(), field:fieldId, op, value:defaultVal(fieldId,op) };
}
function changeConditionOp(cond, newOp) { return { ...cond, op:newOp, value:defaultVal(cond.field,newOp) }; }

function ConditionRow({ cond, onChange, onRemove }) {
  const fd  = FIELDS.find(f=>f.id===cond.field) || FIELDS[0];
  const ops = OPS[fd.type] || [];
  const setField = (fid) => onChange(makeCondition(fid));
  const setOp    = (op)  => onChange(changeConditionOp(cond,op));
  const setVal   = (v)   => onChange({...cond,value:v});

  function ValueInput() {
    if (fd.type==='select') return (
      <select className="pl-select" value={cond.value} onChange={e=>setVal(e.target.value)}>
        {fd.options.map(o=><option key={o.v} value={o.v}>{o.l}</option>)}
      </select>
    );
    if (fd.type==='camelot') return (
      <select className="pl-select" value={cond.value} onChange={e=>setVal(e.target.value)}>
        {CAMELOT_KEYS.map(k=><option key={k} value={k}>{k} · {CAMELOT_LABELS[k]}</option>)}
      </select>
    );
    if (cond.op==='between') {
      const val = Array.isArray(cond.value)?cond.value:[0,100];
      const unit = fd.type==='pct'?'%':'';
      return (
        <span className="pl-range-wrap">
          <input type="number" className="pl-num" value={val[0]??''} onChange={e=>setVal([e.target.value===''?'':+e.target.value,val[1]])} />
          <span className="pl-range-sep">–</span>
          <input type="number" className="pl-num" value={val[1]??''} onChange={e=>setVal([val[0],e.target.value===''?'':+e.target.value])} />
          {unit&&<span className="pl-unit">{unit}</span>}
        </span>
      );
    }
    if (fd.type==='text') return (
      <input className="pl-input" type="text" placeholder="p.ej. Techno" value={cond.value||''} onChange={e=>setVal(e.target.value)} />
    );
    const unit = fd.type==='pct'?'%':'';
    return (
      <span className="pl-range-wrap">
        <input type="number" className="pl-num" value={cond.value??''} onChange={e=>setVal(e.target.value===''?'':+e.target.value)} />
        {unit&&<span className="pl-unit">{unit}</span>}
      </span>
    );
  }

  return (
    <div className="pl-cond-row">
      <select className="pl-select" value={cond.field} onChange={e=>setField(e.target.value)}>
        {FIELDS.map(f=><option key={f.id} value={f.id}>{f.label}</option>)}
      </select>
      <select className="pl-select" value={cond.op} onChange={e=>setOp(e.target.value)}>
        {ops.map(o=><option key={o.id} value={o.id}>{o.l}</option>)}
      </select>
      <ValueInput />
      <button className="pl-remove-btn" onClick={onRemove} title="Eliminar regla">
        <svg width="11" height="11" viewBox="0 0 24 24" fill="none">
          <path d="M6 6l12 12M18 6L6 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
        </svg>
      </button>
    </div>
  );
}

function PlaylistEditor({ playlist, tracks, onSave, onBack }) {
  const [name,       setName]       = useState(playlist?.name || '');
  const [operator,   setOperator]   = useState(playlist?.operator || 'AND');
  const [conditions, setConditions] = useState(playlist?.conditions?.length ? playlist.conditions : [makeCondition('bpm')]);

  const addCond    = ()      => setConditions(prev=>[...prev,makeCondition('bpm')]);
  const removeCond = (id)    => setConditions(prev=>prev.filter(c=>c.id!==id));
  const updateCond = (id,nc) => setConditions(prev=>prev.map(c=>c.id===id?nc:c));
  const matching = useMemo(() => tracks.filter(t=>matchPlaylist(t,{operator,conditions})), [tracks,operator,conditions]);

  const save = () => {
    if (!name.trim()) return;
    const pl = { id:playlist?.id||uid(), name:name.trim(), operator, conditions, createdAt:playlist?.createdAt||Date.now(), updatedAt:Date.now() };
    window.LibraryStore.savePlaylist(pl); onSave(pl);
  };

  return (
    <div className="pl-editor">
      <button className="pl-back" onClick={onBack}>
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
          <path d="M19 12H5M12 5l-7 7 7 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
        </svg>
        Biblioteca
      </button>
      <input className="pl-name-input" type="text" placeholder="Nombre de la playlist…" value={name} onChange={e=>setName(e.target.value)} />
      <div className="pl-operator-row">
        <span className="pl-label">Incluir pistas donde</span>
        <button className={`pl-op-btn${operator==='AND'?' active':''}`} onClick={()=>setOperator('AND')}>TODAS</button>
        <button className={`pl-op-btn${operator==='OR'?' active':''}`}  onClick={()=>setOperator('OR')}>ALGUNA</button>
        <span className="pl-label">condición se cumpla:</span>
      </div>
      <div className="pl-conditions">
        {conditions.map(c=>(
          <ConditionRow key={c.id} cond={c} onChange={nc=>updateCond(c.id,nc)} onRemove={()=>removeCond(c.id)} />
        ))}
        <button className="pl-add-cond" onClick={addCond}>
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none">
            <path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round"/>
          </svg>
          Añadir condición
        </button>
      </div>
      <div className="pl-preview-box">
        <div className="pl-preview-header">
          <span className="pl-label">{matching.length} pista{matching.length!==1?'s':''} coinciden</span>
          {matching.length > 0 && (
            <button className="pl-btn-ghost" onClick={()=>exportM3U(matching,name)}>
              <svg width="13" height="13" viewBox="0 0 24 24" fill="none">
                <path d="M12 4v12M7 11l5 5 5-5M5 20h14" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
              </svg>
              Exportar M3U
            </button>
          )}
        </div>
        {matching.length > 0 ? (
          <>
            <div className="pl-preview-list">
              {matching.slice(0,10).map(t=>(
                <div key={t.id} className="pl-preview-item">
                  <span className="pl-track-name">{t.name}</span>
                  <TrackPills track={t} />
                </div>
              ))}
            </div>
            {matching.length > 10 && <div className="pl-preview-more">… y {matching.length-10} pistas más</div>}
          </>
        ) : (
          <div className="pl-empty-small">
            {tracks.length===0 ? 'La biblioteca está vacía. Importa canciones primero.' : 'Ninguna pista cumple estas condiciones todavía.'}
          </div>
        )}
      </div>
      <div className="pl-editor-actions">
        <button className="pl-btn-ghost" onClick={onBack}>Cancelar</button>
        <button className="pl-btn-primary" onClick={save} disabled={!name.trim()}>Guardar playlist</button>
      </div>
    </div>
  );
}

function PlaylistCard({ pl, tracks, onEdit, onDelete }) {
  const matching = useMemo(() => tracks.filter(t=>matchPlaylist(t,pl)), [tracks,pl]);
  return (
    <div className="pl-card" onClick={()=>onEdit(pl)}>
      <div className="pl-card-body">
        <div className="pl-card-name">{pl.name}</div>
        <div className="pl-cond-pills">
          {pl.conditions.slice(0,3).map(c=>{
            const fd  = FIELDS.find(f=>f.id===c.field);
            const op  = OPS[fd?.type||'numeric']?.find(o=>o.id===c.op);
            const val = Array.isArray(c.value)?`${c.value[0]}–${c.value[1]}`:String(c.value);
            return <span key={c.id} className="pl-cond-tag">{fd?.label} {op?.l} {val}{fd?.type==='pct'?'%':''}</span>;
          })}
          {pl.conditions.length > 3 && <span className="pl-cond-tag">+{pl.conditions.length-3} más</span>}
        </div>
        <div className="pl-card-footer">
          <span className="pl-label">{matching.length} pista{matching.length!==1?'s':''} · {pl.conditions.length} regla{pl.conditions.length!==1?'s':''} · {pl.operator}</span>
        </div>
      </div>
      <div className="pl-card-actions" onClick={e=>e.stopPropagation()}>
        {matching.length > 0 && (
          <button className="pl-btn-ghost" title="Exportar M3U" onClick={()=>exportM3U(matching,pl.name)}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
              <path d="M12 4v12M7 11l5 5 5-5M5 20h14" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
            </svg>
            M3U
          </button>
        )}
        <button className="pl-btn-ghost pl-btn-danger" title="Eliminar" onClick={()=>onDelete(pl.id)}>
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
            <path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2M6 6l1 14a2 2 0 002 2h6a2 2 0 002-2l1-14" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
          </svg>
        </button>
      </div>
    </div>
  );
}

// ── Analysis Queue ────────────────────────────────────────────────────────────
const ANALYSIS_SLOTS = 2;

class AnalysisQueue {
  constructor(onUpdate) {
    this._onUpdate = onUpdate;
    this._pending  = [];
    this._slots    = [];
    this._busy     = [];
    this._onnx     = null;
    this._init();
  }

  _init() {
    for (let i = 0; i < ANALYSIS_SLOTS; i++) {
      const w = new Worker('workers/conversion-worker.js', { type:'module' });
      w.onmessage = (e) => this._onMsg(i, e.data);
      w.onerror   = () => { this._busy[i] = false; this._drain(); };
      this._slots.push(w);
      this._busy.push(false);
    }
    this._onnx = new Worker('workers/onnx-worker.js', { type:'module' });
    this._onnx.onmessage = (e) => this._onOnnx(e.data);
    this._onnx.postMessage({ type:'preload' });
  }

  enqueue(localId, file) {
    this._pending.push({ localId, file });
    this._onUpdate(localId, { status:'queued', position:this._pending.length });
    this._drain();
  }

  _drain() {
    for (let i = 0; i < this._slots.length; i++) {
      if (this._busy[i] || !this._pending.length) continue;
      const job = this._pending.shift();
      this._dispatch(i, job);
      this._pending.forEach((j, idx) => this._onUpdate(j.localId, { position:idx+1 }));
    }
  }

  async _dispatch(slotIdx, { localId, file }) {
    this._busy[slotIdx] = true;
    this._onUpdate(localId, { status:'analyzing', progress:0 });
    const buf = await file.arrayBuffer();
    this._slots[slotIdx].postMessage(
      { type:'analyze_only', jobId:localId, fileBuffer:buf, fileName:file.name },
      [buf]
    );
  }

  _onMsg(slotIdx, msg) {
    const { type, jobId } = msg;
    if (type === 'progress') { this._onUpdate(jobId, { progress:msg.progress }); return; }
    if (type === 'analyzed_only') {
      this._onUpdate(jobId, { status:'done', progress:100, bpm:msg.bpm??null, key:msg.key??null, camelot:msg.camelot??null, sections:msg.sections??null });
      if (msg.patches && msg.patchCount > 0) {
        this._onnx.postMessage({ type:'analyze', jobId, patches:msg.patches, patchCount:msg.patchCount }, [msg.patches.buffer]);
      } else {
        this._onUpdate(jobId, { onnxDone:true });
      }
    }
    if (type === 'error') this._onUpdate(jobId, { status:'error', error:msg.error });
    if (type === 'idle')  { this._busy[slotIdx] = false; this._drain(); }
  }

  _onOnnx(msg) {
    if (msg.type !== 'analyzed') return;
    const patch = { onnxDone:true };
    if (!msg.error) {
      patch.genres       = msg.genres       ?? null;
      patch.danceability = msg.danceability ?? null;
      patch.instrumental = msg.instrumental ?? null;
      patch.embedding    = msg.embedding    ?? null;
      patch.moods = { party:msg.party, aggressive:msg.aggressive, happy:msg.happy, relaxed:msg.relaxed };
    }
    this._onUpdate(msg.jobId, patch);
  }

  destroy() {
    this._slots.forEach(w => w.terminate());
    this._onnx?.terminate();
  }
}

// ── File helpers ──────────────────────────────────────────────────────────────
const IMPORT_SUPPORTED = ['mp3','alac','aac','m4a','mp4','wav','aiff','aif','flac','ape','tta','wv','ogg','opus','wma','dsf','dff','caf'];
const IMPORT_MAX_MB    = 500;
function getExt(name)  { const m=(name||'').toLowerCase().match(/\.([a-z0-9]+)$/); return m?m[1]:''; }
function fmtMB(b)      { return (b/1024/1024).toFixed(1)+' MB'; }
function importUid()   { return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2); }

// ── Drop Hero (empty-library entry point) ─────────────────────────────────────
function DropHero({ onFiles }) {
  const [hot, setHot] = useState(false);
  const inputRef = useRef(null);

  return (
    <div
      style={{ flex:1, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', padding:'40px 24px', minHeight:'62vh' }}
      onDragOver={e=>{e.preventDefault();setHot(true);}}
      onDragEnter={e=>{e.preventDefault();setHot(true);}}
      onDragLeave={()=>setHot(false)}
      onDrop={e=>{e.preventDefault();setHot(false);if(e.dataTransfer.files.length)onFiles(e.dataTransfer.files);}}
    >
      <div style={{
        background: hot ? 'rgba(92,199,255,0.07)' : 'var(--ink-2)',
        border: `2px dashed ${hot?'var(--accent)':'var(--line-2)'}`,
        borderRadius:20, padding:'48px 40px', textAlign:'center',
        maxWidth:500, width:'100%', transition:'border-color .2s, background .2s',
      }}>
        <div className="drop-icon" style={{ marginBottom:16 }}>
          <svg width="38" height="38" viewBox="0 0 24 24" fill="none">
            <path d="M9 19V6l12-3v13M9 19a2 2 0 11-4 0 2 2 0 014 0zm12-2a3 3 0 11-6 0 3 3 0 016 0z" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
          </svg>
        </div>
        <h2 style={{ margin:'0 0 10px', fontSize:22, fontWeight:700 }}>Añade tu música</h2>
        <p style={{ color:'var(--text-3)', margin:'0 0 26px', fontSize:14, lineHeight:1.65 }}>
          BPM · Camelot · Género · Bailabilidad — todo se analiza aquí, sin subir nada
        </p>
        <button
          className="pl-btn-primary"
          style={{ height:46, fontSize:14, padding:'0 24px', display:'inline-flex', alignItems:'center', gap:8 }}
          onClick={() => inputRef.current?.click()}
        >
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M12 4v16M4 12h16" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"/></svg>
          Seleccionar archivos
        </button>
        <input ref={inputRef} type="file" hidden multiple
          accept=".mp3,.alac,.aac,.m4a,.mp4,.wav,.aiff,.aif,.flac,.ape,.tta,.wv,.ogg,.opus,.wma,.dsf,.dff,.caf"
          onChange={e=>{if(e.target.files?.length)onFiles(e.target.files);e.target.value='';}}
        />
        <div style={{ marginTop:18, display:'flex', flexWrap:'wrap', gap:5, justifyContent:'center' }}>
          {['mp3','wav','aiff','flac','alac','aac','ogg','opus','wma','dsf','ape'].map(f=>(
            <span key={f} style={{ fontFamily:'var(--font-mono)', fontSize:10, color:'var(--text-3)', opacity:.7 }}>.{f}</span>
          ))}
        </div>
      </div>
    </div>
  );
}

// ── Add Panel (analysis progress) ─────────────────────────────────────────────
function AddPanel({ initialFiles, onClose, hasLibrary }) {
  const [items,   setItems]   = useState([]);
  const [phase,   setPhase]   = useState('idle');
  const [hot,     setHot]     = useState(false);
  const inputRef        = useRef(null);
  const queueRef        = useRef(null);
  const itemsRef        = useRef([]);
  const savedRef        = useRef(new Set());
  const pendingFilesRef = useRef(new Map());

  useEffect(() => { itemsRef.current = items; }, [items]);

  const patchItem = useCallback((id, patch) => {
    setItems(prev => prev.map(i => i.id === id ? { ...i, ...patch } : i));
  }, []);

  const makeItem = (f) => {
    const ext = getExt(f.name);
    const ok  = IMPORT_SUPPORTED.includes(ext);
    const big = f.size > IMPORT_MAX_MB * 1024 * 1024;
    return {
      id: importUid(), fileName: f.name,
      status: !ok ? 'error' : big ? 'error' : 'pending',
      error:  !ok ? 'Formato no soportado' : big ? `Demasiado grande (${fmtMB(f.size)})` : null,
      progress:0, position:0, bpm:null, key:null, camelot:null, genres:null,
      danceability:null, instrumental:null, moods:null, sections:null, embedding:null, onnxDone:false,
    };
  };

  const addFiles = useCallback((fileList) => {
    const files = Array.from(fileList).slice(0, 200 - itemsRef.current.length);
    const newItems = files.map(f => {
      const item = makeItem(f);
      if (item.status === 'pending') pendingFilesRef.current.set(item.id, f);
      return item;
    });
    if (!newItems.length) return;
    setItems(prev => [...prev, ...newItems]);
    setPhase(p => p === 'idle' ? 'selected' : p);
  }, []);

  useEffect(() => { if (initialFiles?.length) addFiles(initialFiles); }, []);

  // Auto-save when ONNX complete
  useEffect(() => {
    for (const item of items) {
      if (item.status !== 'done' || !item.onnxDone || savedRef.current.has(item.id)) continue;
      savedRef.current.add(item.id);
      window.LibraryStore.addTrack({
        id:           item.id,
        name:         item.fileName.replace(/\.[^.]+$/, ''),
        filename:     item.fileName,
        format:       getExt(item.fileName),
        addedAt:      Date.now(),
        bpm:          item.bpm          ?? null,
        key:          item.key          ?? null,
        camelot:      item.camelot      ?? null,
        genres:       item.genres       ?? null,
        danceability: item.danceability ?? null,
        instrumental: item.instrumental ?? null,
        moods:        item.moods        ?? null,
        sections:     item.sections     ?? null,
        embedding:    item.embedding    ?? null,
      });
    }
    const allDone = items.length > 0 && items.every(i => i.status === 'done' || i.status === 'error');
    if (allDone && phase === 'analyzing') setPhase('done');
  }, [items, phase]);

  useEffect(() => () => queueRef.current?.destroy(), []);

  const startAnalysis = () => {
    const toAnalyze = itemsRef.current.filter(i => i.status === 'pending');
    if (!toAnalyze.length) return;
    setPhase('analyzing');
    if (!queueRef.current) queueRef.current = new AnalysisQueue(patchItem);
    toAnalyze.forEach(item => {
      const file = pendingFilesRef.current.get(item.id);
      if (file) queueRef.current.enqueue(item.id, file);
    });
  };

  const reset = () => {
    queueRef.current?.destroy(); queueRef.current = null;
    setItems([]); setPhase('idle');
    savedRef.current = new Set(); pendingFilesRef.current = new Map();
  };

  const pending = items.filter(i => i.status === 'pending');
  const done    = items.filter(i => i.status === 'done');
  const errors  = items.filter(i => i.status === 'error');
  const running = phase === 'analyzing';

  const StatusIcon = ({ item }) => {
    if (item.status === 'done' && item.onnxDone)  return <span style={{ color:'#96f582', fontSize:14, width:20, textAlign:'center' }}>✓</span>;
    if (item.status === 'done')   return <span style={{ fontSize:10, color:'var(--text-3)', fontFamily:'var(--font-mono)', width:20, textAlign:'center' }}>…</span>;
    if (item.status === 'error')  return <span style={{ color:'#eb4baf', fontSize:14, width:20, textAlign:'center' }}>✗</span>;
    if (item.status === 'analyzing') return <span style={{ fontSize:10, color:'var(--accent)', fontFamily:'var(--font-mono)', width:20, textAlign:'center' }}>{item.progress}%</span>;
    if (item.status === 'queued') return <span style={{ fontSize:10, color:'var(--text-3)', fontFamily:'var(--font-mono)', width:20, textAlign:'center' }}>#{item.position}</span>;
    return <span style={{ width:20 }} />;
  };

  return (
    <div style={{
      background:'var(--ink-2)', border:'1px solid var(--line-2)', borderRadius:14,
      padding:'18px 20px', marginBottom: hasLibrary ? 20 : 0,
    }}>
      {/* Header row */}
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:14 }}>
        <div style={{ display:'flex', alignItems:'center', gap:10 }}>
          <span style={{ fontWeight:600, fontSize:15 }}>Añadir canciones</span>
          {items.length > 0 && (
            <span style={{ fontFamily:'var(--font-mono)', fontSize:11, color:'var(--text-3)' }}>
              {items.length} archivo{items.length!==1?'s':''}
              {running && ` · analizando ${done.length}/${items.length-errors.length}`}
              {phase==='done' && ` · ${done.length} guardadas`}
            </span>
          )}
        </div>
        <button onClick={() => { reset(); onClose(); }} style={{
          background:'none', border:'1px solid var(--line-2)', borderRadius:6,
          color:'var(--text-3)', padding:'4px 10px', cursor:'pointer', fontSize:12,
        }}>
          {phase === 'done' ? '← Volver' : 'Cancelar'}
        </button>
      </div>

      {/* Privacy note */}
      <div style={{ background:'rgba(92,199,255,0.06)', border:'1px solid rgba(92,199,255,0.18)', borderRadius:8, padding:'7px 12px', marginBottom:14, fontSize:12, color:'var(--text-2)', display:'flex', gap:8, alignItems:'center' }}>
        <span>🔒</span>
        <span>Análisis local · tus archivos no se suben · los originales no se modifican</span>
      </div>

      {/* Drop zone when no items yet */}
      {items.length === 0 && (
        <div
          onDragOver={e=>{e.preventDefault();setHot(true);}}
          onDragEnter={e=>{e.preventDefault();setHot(true);}}
          onDragLeave={()=>setHot(false)}
          onDrop={e=>{e.preventDefault();setHot(false);addFiles(e.dataTransfer.files);}}
          onClick={() => inputRef.current?.click()}
          style={{
            border:`2px dashed ${hot?'var(--accent)':'var(--line-2)'}`,
            borderRadius:10, padding:'28px 20px', textAlign:'center',
            background: hot?'rgba(92,199,255,0.05)':'transparent',
            transition:'border-color .2s,background .2s', cursor:'pointer',
          }}
        >
          <div style={{ color:'var(--text-3)', fontSize:13, marginBottom:10 }}>Arrastra archivos aquí o</div>
          <button className="pl-btn-ghost" type="button" onClick={e=>{e.stopPropagation();inputRef.current?.click();}}>
            Seleccionar archivos
          </button>
        </div>
      )}

      <input ref={inputRef} type="file" hidden multiple
        accept=".mp3,.alac,.aac,.m4a,.mp4,.wav,.aiff,.aif,.flac,.ape,.tta,.wv,.ogg,.opus,.wma,.dsf,.dff,.caf"
        onChange={e=>{addFiles(e.target.files);e.target.value='';}}
      />

      {items.length > 0 && (
        <>
          {/* Action bar */}
          {!running && phase !== 'done' && (
            <div style={{ display:'flex', gap:8, marginBottom:12, flexWrap:'wrap' }}>
              <button className="pl-btn-primary" onClick={startAnalysis} disabled={!pending.length}>
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none"><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/></svg>
                Analizar {pending.length} pista{pending.length!==1?'s':''}
              </button>
              <button className="pl-btn-ghost" style={{fontSize:12}} onClick={()=>inputRef.current?.click()}>+ Añadir más</button>
            </div>
          )}

          {/* File list */}
          <div style={{ display:'flex', flexDirection:'column', gap:4, maxHeight:300, overflowY:'auto' }}>
            {items.map(item => (
              <div key={item.id} style={{
                display:'flex', alignItems:'center', gap:8, padding:'7px 11px',
                background:'var(--ink-3)', borderRadius:7, border:'1px solid var(--line)',
              }}>
                <StatusIcon item={item} />
                <div style={{ flex:1, minWidth:0 }}>
                  <div style={{ fontSize:13, fontWeight:500, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{item.fileName}</div>
                  {item.status === 'done' && (
                    <div style={{ display:'flex', gap:4, flexWrap:'wrap', marginTop:3 }}>
                      {item.bpm && <span className="fc-pill">{item.bpm} BPM</span>}
                      {item.camelot && <span className="fc-pill">{item.camelot}</span>}
                      {item.danceability!=null && <span className="fc-pill">💃 {Math.round(item.danceability*100)}%</span>}
                      {item.instrumental!=null && <span className="fc-pill fc-pill-voice">{item.instrumental>=0.5?'Inst':'Vocal'}</span>}
                      {item.genres?.slice(0,2).map((g,i)=><span key={i} className="fc-pill fc-pill-genre">{subGenre(g)}</span>)}
                      {!item.onnxDone && <span className="fc-pill fc-pill-analyzing">Descubriendo estilo…</span>}
                    </div>
                  )}
                  {item.status === 'analyzing' && (
                    <div style={{ marginTop:4, height:3, background:'var(--ink-4)', borderRadius:2 }}>
                      <div style={{ width:`${item.progress}%`, height:'100%', background:'var(--accent)', borderRadius:2, transition:'width .2s' }} />
                    </div>
                  )}
                  {item.status === 'error' && (
                    <div style={{ fontSize:11, color:'#eb4baf', marginTop:2 }}>{item.error}</div>
                  )}
                </div>
              </div>
            ))}
          </div>

          {phase === 'done' && (
            <div style={{ display:'flex', gap:10, marginTop:14, flexWrap:'wrap', alignItems:'center' }}>
              <span style={{ fontFamily:'var(--font-mono)', fontSize:12, color:'#96f582' }}>✓ {done.length} guardadas en biblioteca</span>
              <button className="pl-btn-ghost" style={{fontSize:12}} onClick={reset}>Añadir más</button>
              <button className="pl-btn-primary" onClick={() => { onClose(); }}>Ver biblioteca →</button>
            </div>
          )}
        </>
      )}
    </div>
  );
}

// ── Playlists Section ─────────────────────────────────────────────────────────
function PlaylistsSection({ playlists, tracks, onRefresh, onEdit }) {
  const handleDelete = (id) => {
    if (!confirm('¿Eliminar esta playlist?')) return;
    window.LibraryStore.deletePlaylist(id); onRefresh();
  };
  return (
    <div style={{ marginTop:24, paddingTop:20, borderTop:'1px solid var(--line)' }}>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:14 }}>
        <div style={{ fontFamily:'var(--font-mono)', fontSize:11, letterSpacing:'.12em', color:'var(--text-3)', textTransform:'uppercase' }}>
          Playlists inteligentes{playlists.length>0?` · ${playlists.length}`:''}
        </div>
        <button className="pl-btn-primary" style={{fontSize:12}} onClick={() => onEdit({})}>
          <svg width="11" height="11" viewBox="0 0 24 24" fill="none"><path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round"/></svg>
          Nueva playlist
        </button>
      </div>
      {playlists.length === 0 ? (
        <div className="pl-empty-small">Crea una playlist para filtrar tu música por BPM, clave, género o mood.</div>
      ) : (
        <div className="pl-card-list">
          {playlists.map(pl => <PlaylistCard key={pl.id} pl={pl} tracks={tracks} onEdit={onEdit} onDelete={handleDelete} />)}
        </div>
      )}
    </div>
  );
}

// ── Doctor Section ────────────────────────────────────────────────────────────
function DoctorSection({ tracks }) {
  const health = useMemo(() => Eng ? Eng.getLibraryHealth(tracks) : null, [tracks]);
  if (!health) return null;

  const withBpm = tracks.filter(t => t.bpm);
  const withCam = tracks.filter(t => t.camelot);
  const camFreq = {};
  withCam.forEach(t => { camFreq[t.camelot] = (camFreq[t.camelot]||0)+1; });
  const topCam = Object.entries(camFreq).sort((a,b)=>b[1]-a[1]).slice(0,4);

  return (
    <div style={{ marginTop:24, paddingTop:20, borderTop:'1px solid var(--line)' }}>
      <div style={{ fontFamily:'var(--font-mono)', fontSize:11, letterSpacing:'.12em', color:'var(--text-3)', textTransform:'uppercase', marginBottom:16 }}>Library Doctor</div>

      <div style={{ background:'var(--ink-3)', borderRadius:12, padding:'16px 18px', border:'1px solid var(--line)', marginBottom:12, display:'flex', gap:18, alignItems:'center', flexWrap:'wrap' }}>
        <div>
          <div style={{ fontSize:42, fontWeight:700, color:scoreColor(health.score), fontFamily:'var(--font-mono)', lineHeight:1 }}>
            {health.score}<span style={{ fontSize:20, color:'var(--text-3)' }}>/100</span>
          </div>
          <div style={{ fontSize:11, color:'var(--text-3)', marginTop:4 }}>Salud de biblioteca</div>
        </div>
        <div style={{ flex:1, minWidth:160 }}>
          <div style={{ height:7, background:'var(--ink-4)', borderRadius:4, marginBottom:10, overflow:'hidden' }}>
            <div style={{ width:`${health.score}%`, height:'100%', background:scoreColor(health.score), borderRadius:4 }} />
          </div>
          {health.strengths.map((s,i) => <div key={i} style={{ fontSize:12, color:'#96f582', marginBottom:3 }}>✓ {s}</div>)}
        </div>
      </div>

      {health.issues.length > 0 && (
        <div style={{ marginBottom:12 }}>
          {health.issues.map((issue,i) => (
            <div key={i} style={{
              display:'flex', gap:8, alignItems:'flex-start', padding:'8px 12px',
              background: issue.sev==='warn'?'rgba(245,166,35,0.07)':'var(--ink-3)',
              border: `1px solid ${issue.sev==='warn'?'rgba(245,166,35,0.22)':'var(--line)'}`,
              borderRadius:8, marginBottom:5, fontSize:13,
              color: issue.sev==='warn'?'#f5a623':'var(--text-2)',
            }}>
              <span>{issue.sev==='warn'?'⚠':'ℹ'}</span><span>{issue.msg}</span>
            </div>
          ))}
        </div>
      )}

      <div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill,minmax(140px,1fr))', gap:10, marginBottom:12 }}>
        <div style={{ background:'var(--ink-3)', borderRadius:10, padding:'12px 14px', border:'1px solid var(--line)' }}>
          <div style={{ fontFamily:'var(--font-mono)', fontSize:10, color:'var(--text-3)', textTransform:'uppercase', marginBottom:5 }}>BPM</div>
          <div style={{ fontFamily:'var(--font-mono)', fontSize:20, fontWeight:700 }}>{withBpm.length}/{tracks.length}</div>
          {withBpm.length < tracks.length && <div style={{ fontSize:11, color:'#f5a623', marginTop:2 }}>{tracks.length-withBpm.length} sin BPM</div>}
        </div>
        <div style={{ background:'var(--ink-3)', borderRadius:10, padding:'12px 14px', border:'1px solid var(--line)' }}>
          <div style={{ fontFamily:'var(--font-mono)', fontSize:10, color:'var(--text-3)', textTransform:'uppercase', marginBottom:5 }}>Camelot</div>
          <div style={{ fontFamily:'var(--font-mono)', fontSize:20, fontWeight:700 }}>{withCam.length}/{tracks.length}</div>
          {withCam.length < tracks.length && <div style={{ fontSize:11, color:'#f5a623', marginTop:2 }}>{tracks.length-withCam.length} sin Camelot</div>}
        </div>
        <div style={{ background:'var(--ink-3)', borderRadius:10, padding:'12px 14px', border:'1px solid var(--line)' }}>
          <div style={{ fontFamily:'var(--font-mono)', fontSize:10, color:'var(--text-3)', textTransform:'uppercase', marginBottom:6 }}>Top Camelot</div>
          {topCam.map(([k,n]) => (
            <div key={k} style={{ display:'flex', justifyContent:'space-between', marginBottom:3 }}>
              <span style={{ fontFamily:'var(--font-mono)', fontSize:11, color:'var(--accent)' }}>{k}</span>
              <span style={{ fontSize:11, color:'var(--text-3)' }}>{n}</span>
            </div>
          ))}
        </div>
      </div>

      {health.roleRecs.length > 0 && (
        <div style={{ background:'var(--ink-3)', borderRadius:12, padding:'16px 18px', border:'1px solid var(--line)' }}>
          <div style={{ fontFamily:'var(--font-mono)', fontSize:10, color:'var(--text-3)', textTransform:'uppercase', marginBottom:10 }}>Mejores pistas por rol</div>
          {health.roleRecs.map(({ role, track, score:s }) => (
            <div key={role} style={{ display:'flex', gap:10, alignItems:'center', padding:'7px 10px', background:'var(--ink-4)', borderRadius:7, marginBottom:5 }}>
              <span style={{ background:roleColor(role)+'22', color:roleColor(role), border:`1px solid ${roleColor(role)}44`, borderRadius:5, padding:'2px 8px', fontSize:11, fontWeight:600, minWidth:54, textAlign:'center' }}>{ROLE_LABELS[role]}</span>
              <span style={{ flex:1, fontSize:13, fontWeight:500, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{track.name}</span>
              {track.bpm    && <span style={{ fontFamily:'var(--font-mono)', fontSize:10, color:'var(--text-3)' }}>{track.bpm}</span>}
              {track.camelot && <span style={{ fontFamily:'var(--font-mono)', fontSize:10, color:'var(--text-3)' }}>{track.camelot}</span>}
              <span style={{ fontFamily:'var(--font-mono)', fontSize:11, color:scoreColor(s) }}>{s}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ── Library View ──────────────────────────────────────────────────────────────
function LibraryView({ tracks, playlists, onRefresh, onAdd, onEditPlaylist }) {
  const [query,        setQuery]        = useState('');
  const [sortBy,       setSortBy]       = useState('score');
  const [sortDir,      setSortDir]      = useState('desc');
  const [dnaTrack,     setDnaTrack]     = useState(null);
  const [showPlaylists, setShowPlaylists] = useState(false);
  const [showDoctor,   setShowDoctor]   = useState(false);

  const filtered = useMemo(() => {
    let list = tracks;
    if (query.trim()) {
      const q = query.toLowerCase();
      list = list.filter(t =>
        t.name.toLowerCase().includes(q) ||
        t.genres?.some(g => subGenre(g).toLowerCase().includes(q)) ||
        t.camelot?.toLowerCase() === q ||
        String(t.bpm||'') === q
      );
    }
    const dir = sortDir === 'asc' ? 1 : -1;
    return [...list].sort((a, b) => {
      if (sortBy === 'date')  return dir * (a.addedAt - b.addedAt);
      if (sortBy === 'bpm')   return dir * ((a.bpm||0) - (b.bpm||0));
      if (sortBy === 'name')  return dir * a.name.localeCompare(b.name);
      if (sortBy === 'dance') return dir * ((a.danceability||0) - (b.danceability||0));
      if (sortBy === 'score' && Eng) return dir * (Eng.getAntonicScore(a) - Eng.getAntonicScore(b));
      return 0;
    });
  }, [tracks, query, sortBy, sortDir]);

  const toggleSort = (col) => {
    if (sortBy === col) setSortDir(d => d === 'asc' ? 'desc' : 'asc');
    else { setSortBy(col); setSortDir('desc'); }
  };

  const SortBtn = ({ col, label }) => (
    <button onClick={() => toggleSort(col)} style={{
      background:'none', border:'none', cursor:'pointer',
      fontFamily:'var(--font-mono)', fontSize:10, letterSpacing:'.1em', textTransform:'uppercase',
      color: sortBy===col ? 'var(--text-1)' : 'var(--text-3)', padding:'2px 4px',
      display:'flex', alignItems:'center', gap:2,
    }}>
      {label}{sortBy===col && <span style={{opacity:.6}}>{sortDir==='asc'?'↑':'↓'}</span>}
    </button>
  );

  const Btn = ({ active, danger, onClick, children }) => (
    <button onClick={onClick} style={{
      padding:'5px 11px', borderRadius:7, fontSize:12, cursor:'pointer',
      border: `1px solid ${danger?'rgba(235,75,175,0.3)':active?'rgba(92,199,255,0.5)':'var(--line-2)'}`,
      background: active ? 'rgba(92,199,255,0.1)' : 'transparent',
      color: danger ? '#eb4baf' : active ? '#5cc7ff' : 'var(--text-2)',
      fontFamily:'var(--font-mono)', letterSpacing:'.04em',
      display:'flex', alignItems:'center', gap:5, whiteSpace:'nowrap',
    }}>{children}</button>
  );

  const remove   = (id) => { window.LibraryStore.removeTrack(id); onRefresh(); };
  const clearAll = () => {
    if (!confirm(`¿Vaciar la biblioteca? Se eliminarán ${tracks.length} pistas.`)) return;
    window.LibraryStore.clearTracks(); onRefresh();
  };

  return (
    <div>
      {dnaTrack && <TrackDNA track={dnaTrack} allTracks={tracks} onClose={() => setDnaTrack(null)} />}

      {/* Toolbar */}
      <div style={{ display:'flex', gap:7, marginBottom:12, flexWrap:'wrap', alignItems:'center' }}>
        <div className="pl-search-wrap" style={{ flex:'1 1 180px', minWidth:140 }}>
          <svg className="pl-search-icon" width="13" height="13" viewBox="0 0 24 24" fill="none">
            <circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="2"/>
            <path d="M20 20l-3.5-3.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
          </svg>
          <input className="pl-search" placeholder="Buscar…" value={query} onChange={e=>setQuery(e.target.value)} />
        </div>
        <Btn onClick={onAdd}>
          <svg width="11" height="11" viewBox="0 0 24 24" fill="none"><path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"/></svg>
          Añadir
        </Btn>
        <Btn active={showPlaylists} onClick={() => setShowPlaylists(p=>!p)}>
          Playlists{playlists.length>0?` (${playlists.length})`:''}
        </Btn>
        <Btn active={showDoctor} onClick={() => setShowDoctor(p=>!p)}>Doctor</Btn>
        <Btn onClick={() => doExportCSV(tracks)}>CSV</Btn>
        <Btn onClick={() => doExportJSON(tracks)}>JSON</Btn>
        <Btn danger onClick={clearAll}>Vaciar</Btn>
      </div>

      {/* Count + sort */}
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:10, flexWrap:'wrap', gap:6 }}>
        <span style={{ fontFamily:'var(--font-mono)', fontSize:11, color:'var(--text-3)' }}>
          {filtered.length}{filtered.length!==tracks.length?` de ${tracks.length}`:''} pista{tracks.length!==1?'s':''}
        </span>
        <div style={{ display:'flex', gap:4 }}>
          <SortBtn col="score" label="Score" />
          <SortBtn col="bpm"   label="BPM" />
          <SortBtn col="dance" label="Dance" />
          <SortBtn col="name"  label="Nombre" />
          <SortBtn col="date"  label="Fecha" />
        </div>
      </div>

      {/* Track list */}
      <div className="pl-track-list">
        {filtered.length === 0 && query.trim() ? (
          <div style={{ padding:'24px 0', textAlign:'center', color:'var(--text-3)', fontSize:13 }}>Sin resultados para "{query}"</div>
        ) : (
          filtered.map(track => {
            const score  = Eng ? Eng.getAntonicScore(track) : null;
            const roles  = Eng ? Eng.getDjRoles(track)     : null;
            const isInst = track.instrumental != null ? track.instrumental >= 0.5 : null;
            return (
              <div key={track.id} className="pl-track-row" style={{ alignItems:'flex-start', gap:10 }}>
                {score != null && (
                  <div style={{ minWidth:30, textAlign:'center', paddingTop:2, flexShrink:0 }}>
                    <div style={{ fontFamily:'var(--font-mono)', fontSize:14, fontWeight:700, color:scoreColor(score) }}>{score}</div>
                  </div>
                )}
                <div className="pl-track-main" style={{ flex:1, minWidth:0 }}>
                  <span className="pl-track-name" title={track.name}>{track.name}</span>
                  <div style={{ display:'flex', gap:5, flexWrap:'wrap', marginTop:4 }}>
                    {track.bpm        && <span className="fc-pill">{track.bpm} BPM</span>}
                    {track.camelot    && <span className="fc-pill">{track.camelot}</span>}
                    {track.danceability!=null && <span className="fc-pill">💃 {Math.round(track.danceability*100)}%</span>}
                    {isInst != null   && <span className="fc-pill fc-pill-voice">{isInst?'Inst':'Vocal'}</span>}
                    {track.genres?.slice(0,1).map((g,i) => <span key={i} className="fc-pill fc-pill-genre">{subGenre(g)}</span>)}
                    {roles && (
                      <span style={{ background:roleColor(roles.primaryRole)+'22', color:roleColor(roles.primaryRole), border:`1px solid ${roleColor(roles.primaryRole)}44`, borderRadius:5, padding:'2px 7px', fontSize:11, fontWeight:600 }}>
                        {ROLE_LABELS[roles.primaryRole]}
                      </span>
                    )}
                  </div>
                </div>
                <div style={{ flexShrink:0, display:'flex', gap:6, alignItems:'center' }}>
                  <button className="pl-btn-ghost" style={{ fontSize:11, padding:'3px 8px' }} onClick={() => setDnaTrack(track)}>DNA</button>
                  <span className="pl-track-date">{fmtDate(track.addedAt)}</span>
                  <button className="pl-remove-btn" onClick={() => remove(track.id)} title="Eliminar">
                    <svg width="10" height="10" viewBox="0 0 24 24" fill="none">
                      <path d="M6 6l12 12M18 6L6 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
                    </svg>
                  </button>
                </div>
              </div>
            );
          })
        )}
      </div>

      {showPlaylists && (
        <PlaylistsSection playlists={playlists} tracks={tracks} onRefresh={onRefresh} onEdit={onEditPlaylist} />
      )}
      {showDoctor && <DoctorSection tracks={tracks} />}
    </div>
  );
}

// ── Main Page ─────────────────────────────────────────────────────────────────
function PlaylistPage() {
  const [tracks,          setTracks]          = useState([]);
  const [playlists,       setPlaylists]       = useState([]);
  const [adding,          setAdding]          = useState(false);
  const [initialFiles,    setInitialFiles]    = useState(null);
  const [editingPlaylist, setEditingPlaylist] = useState(null);
  const [loading,         setLoading]         = useState(false);

  const refresh = useCallback(() => {
    setTracks(window.LibraryStore.getTracks());
    setPlaylists(window.LibraryStore.getPlaylists());
  }, []);

  useEffect(() => {
    const p = window.LibraryStore?.init?.();
    if (p) {
      p.then(refresh).catch(refresh).finally(() => setLoading(false));
    } else {
      refresh(); setLoading(false);
    }
  }, []);

  useEffect(() => {
    const handler = (e) => { if (e.key === 'antonic_signal') refresh(); };
    window.addEventListener('storage', handler);
    return () => window.removeEventListener('storage', handler);
  }, [refresh]);

  useEffect(() => {
    if (adding) window.scrollTo({ top: 0, behavior: 'smooth' });
  }, [adding]);

  const hasLibrary = tracks.length > 0;

  const Header = () => (
    <div className="pl-header">
      <a href="/" className="pl-brand" style={{ textDecoration:'none', color:'inherit' }}>
        <div className="brand-mark" aria-hidden />
        <span className="brand-name">DJ <b>Antønic</b></span>
      </a>
      <div style={{ display:'flex', gap:12, alignItems:'center' }}>
        {hasLibrary && (
          <span style={{ fontFamily:'var(--font-mono)', fontSize:10, letterSpacing:'.1em', color:'var(--text-3)', textTransform:'uppercase' }}>
            {tracks.length} pista{tracks.length!==1?'s':''}
            {playlists.length>0 && ` · ${playlists.length} playlist${playlists.length!==1?'s':''}`}
          </span>
        )}
        <a href="/" className="pl-link" style={{ fontSize:13 }}>← Convertidor</a>
      </div>
    </div>
  );

  if (loading) return (
    <div className="pl-page">
      <Header />
      <div style={{ padding:'80px 0', textAlign:'center', color:'var(--text-3)', fontFamily:'var(--font-mono)', fontSize:12, letterSpacing:'.1em' }}>Cargando biblioteca…</div>
    </div>
  );

  // Playlist editor takes over
  if (editingPlaylist !== null) return (
    <div className="pl-page">
      <Header />
      <div style={{ maxWidth:720, margin:'0 auto' }}>
        <PlaylistEditor
          playlist={editingPlaylist?.id ? editingPlaylist : null}
          tracks={tracks}
          onSave={() => { refresh(); setEditingPlaylist(null); }}
          onBack={() => setEditingPlaylist(null)}
        />
      </div>
    </div>
  );

  return (
    <div className="pl-page">
      <Header />

      {/* Add panel */}
      {adding && (
        <AddPanel
          initialFiles={initialFiles}
          hasLibrary={hasLibrary}
          onClose={() => { setAdding(false); setInitialFiles(null); refresh(); }}
        />
      )}

      {/* Empty state */}
      {!hasLibrary && !adding && (
        <DropHero onFiles={(files) => { setInitialFiles(files); setAdding(true); }} />
      )}

      {/* Library */}
      {hasLibrary && (
        <LibraryView
          tracks={tracks}
          playlists={playlists}
          onRefresh={refresh}
          onAdd={() => { setInitialFiles(null); setAdding(true); }}
          onEditPlaylist={setEditingPlaylist}
        />
      )}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<PlaylistPage />);
