const { useState, useEffect, useRef, useCallback } = React;

const SUPPORTED = [
  "mp3","alac","aac","m4a","mp4",
  "wav","aiff","aif",
  "flac","ape","tta","wv",
  "ogg","opus","wma","dsf","dff","caf",
];

const OUTPUT_FORMATS = [
  { id: "mp3",  label: "MP3",  ext: "mp3",  lossless: false, kbps: [128,192,256,320], sub: "Universal"      },
  { id: "aac",  label: "AAC",  ext: "m4a",  lossless: false, kbps: [128,192,256,320], sub: "Apple / stream" },
  { id: "ogg",  label: "OGG",  ext: "ogg",  lossless: false, kbps: [128,192,256,320], sub: "Open · web"     },
  { id: "opus", label: "OPUS", ext: "opus", lossless: false, kbps: [64,128,192,256],  sub: "Eficiente"      },
  { id: "flac", label: "FLAC", ext: "flac", lossless: true,  kbps: null, sub: "Sin pérdida"                 },
  { id: "wav",  label: "WAV",  ext: "wav",  lossless: true,  kbps: null, sub: "Estudio · PCM"              },
  { id: "aiff", label: "AIFF", ext: "aiff", lossless: true,  kbps: null, sub: "DJ · Rekordbox"             },
  { id: "alac", label: "ALAC", ext: "m4a",  lossless: true,  kbps: null, sub: "Apple lossless"             },
];

const QUALITY_LABELS = {
  64:  { label: "Ligero",       sub: "voz / podcast" },
  128: { label: "Estándar",     sub: "balance"        },
  192: { label: "Alta calidad", sub: "nítido"         },
  256: { label: "Muy alta",     sub: "detalle fino"   },
  320: { label: "Máxima",       sub: "recomendado"    },
};

const MAX_BYTES       = 500 * 1024 * 1024;
const MAX_FILES       = 100;
const MAX_TOTAL_BYTES = 2 * 1024 * 1024 * 1024;

// Conservative: 2 slots on low-memory devices, 3 on capable ones
const MAX_CONCURRENT = (() => {
  const mem = navigator.deviceMemory; // undefined on iOS/Firefox
  if (mem && mem < 4) return 2;
  return typeof SharedArrayBuffer !== 'undefined' ? 3 : 2;
})();

function fmtBytes(n) {
  if (n < 1024 * 1024) return (n / 1024).toFixed(0) + " KB";
  return (n / 1024 / 1024).toFixed(1) + " MB";
}
function getExt(name) {
  const m = (name || "").toLowerCase().match(/\.([a-z0-9]+)$/);
  return m ? m[1] : "";
}
function uid() { return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2); }

// ── Client Worker Queue ────────────────────────────────────────────────────────
// Creates a pool of conversion workers + one ONNX worker.
// Two-phase completion: phase 1 = converted (download available), phase 2 = analyzed (style info shown).
class ClientQueue {
  constructor(onUpdate) {
    this._onUpdate = onUpdate; // (localId, patch) => void
    this._pending  = [];       // { localId, file, format, quality }
    this._slots    = [];       // Web Workers
    this._busy     = [];       // boolean[]
    this._blobs    = new Map();// localId -> { url, ext }
    this._jobMeta  = new Map();// localId -> { fmt, size }
    this._onnx     = null;
    this._pendingAnalysis = new Map(); // localId -> true (waiting for onnx)
    this._init();
  }

  _init() {
    // Conversion workers
    for (let i = 0; i < MAX_CONCURRENT; i++) {
      const w = new Worker('workers/conversion-worker.js', { type: 'module' });
      w.onmessage = (e) => this._onConvMsg(i, e.data);
      w.onerror   = (e) => this._onConvError(i, e);
      this._slots.push(w);
      this._busy.push(false);
    }
    // ONNX worker (single, serial)
    this._onnx = new Worker('workers/onnx-worker.js', { type: 'module' });
    this._onnx.onmessage = (e) => this._onOnnxMsg(e.data);
    this._onnx.postMessage({ type: 'preload' });
  }

  enqueue(localId, file, format, quality, audioOptions = {}) {
    this._pending.push({ localId, file, format, quality, audioOptions });
    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);
      // Update positions for remaining pending items
      this._pending.forEach((j, idx) => this._onUpdate(j.localId, { position: idx + 1 }));
    }
  }

  async _dispatch(slotIdx, { localId, file, format, quality, audioOptions }) {
    this._busy[slotIdx] = true;
    this._jobMeta.set(localId, { fmt: format, size: file.size || 0 });
    this._onUpdate(localId, { status: 'converting', progress: 0 });

    // Read file to ArrayBuffer and transfer ownership to worker
    const fileBuffer = await file.arrayBuffer();
    this._slots[slotIdx].postMessage(
      { type: 'convert', jobId: localId, fileBuffer, fileName: file.name, format, quality, audioOptions },
      [fileBuffer]
    );
  }

  _onConvMsg(slotIdx, msg) {
    const { type, jobId: localId } = msg;

    if (type === 'progress') {
      this._onUpdate(localId, { progress: msg.progress });
      return;
    }

    if (type === 'converted') {
      this._blobs.set(localId, { url: msg.blobUrl, blob: msg.blob, ext: msg.ext });
      const meta = this._jobMeta.get(localId);
      if (meta) {
        fetch('api/auth/track', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify(meta) }).catch(() => {});
        this._jobMeta.delete(localId);
      }
      // Phase 1 complete: mark done, download available
      this._onUpdate(localId, {
        status: 'done', progress: 100,
        bpm: msg.bpm ?? null, key: msg.key ?? null, camelot: msg.camelot ?? null,
        sections: msg.sections ?? null,
      });
      // Phase 2: send patches to ONNX worker if available
      if (msg.patches && msg.patchCount > 0) {
        this._pendingAnalysis.set(localId, true);
        this._onnx.postMessage(
          { type: 'analyze', jobId: localId, patches: msg.patches, patchCount: msg.patchCount },
          [msg.patches.buffer]
        );
      } else {
        this._onUpdate(localId, { onnxDone: true }); // no audio patches → skip ONNX
      }
    }

    if (type === 'error') {
      this._onUpdate(localId, { status: 'error', error: msg.error });
    }

    if (type === 'idle') {
      this._busy[slotIdx] = false;
      this._drain();
    }
  }

  _onConvError(slotIdx, e) {
    console.error('[ConvWorker] crash:', e);
    this._busy[slotIdx] = false;
    this._drain();
  }

  _onOnnxMsg(msg) {
    if (msg.type !== 'analyzed') return;
    this._pendingAnalysis.delete(msg.jobId);
    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);
  }

  getBlob(localId) { return this._blobs.get(localId) || null; }

  revokeBlob(localId) {
    const b = this._blobs.get(localId);
    if (b) { URL.revokeObjectURL(b.url); this._blobs.delete(localId); }
  }

  destroy() {
    this._slots.forEach(w => w.terminate());
    this._onnx.terminate();
    this._blobs.forEach(b => URL.revokeObjectURL(b.url));
  }
}

// ── FileCard ──────────────────────────────────────────────────────────────────
function FileCard({ item, onRemove, locked }) {
  const ext  = getExt(item.file.name).toUpperCase().slice(0, 5) || "FILE";
  const name = item.file.name.replace(/\.[^.]+$/, "");

  return (
    <div className={`fc fc-${item.status}`}>
      <div className="fc-top">
        <span className="fc-badge">{ext}</span>
        <span className="fc-name" title={name}>{name}</span>
        {item.status === "done" && (
          <svg className="fc-icon-done" width="14" height="14" viewBox="0 0 24 24" fill="none">
            <path d="M5 12l5 5L20 7" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
          </svg>
        )}
        {item.status === "error" && (
          <svg className="fc-icon-err" width="14" height="14" viewBox="0 0 24 24" fill="none">
            <path d="M6 6l12 12M18 6L6 18" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"/>
          </svg>
        )}
        {!locked && (item.status === "pending" || item.status === "error") && (
          <button className="fc-remove" aria-label="Quitar" onClick={() => onRemove(item.localId)}>
            <svg width="11" height="11" viewBox="0 0 24 24" fill="none">
              <path d="M6 6l12 12M18 6L6 18" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"/>
            </svg>
          </button>
        )}
      </div>

      {(item.status === "queued" || item.status === "converting") && (
        <div className="fc-prog-wrap">
          <div className="fc-prog-bar">
            <div className="fc-prog-fill" style={{ width: item.status === "queued" ? "4%" : `${item.progress || 4}%` }} />
          </div>
          <span className="fc-prog-label">
            {item.status === "queued" ? `#${item.position || "–"}` : `${item.progress || 0}%`}
          </span>
        </div>
      )}

      {item.status === "done" && (
        <div className="fc-meta">
          {item.bpm      && <span className="fc-pill">{item.bpm} BPM</span>}
          {item.camelot  && <span className="fc-pill">{item.camelot}</span>}
          {item.genres   && item.genres.slice(0, 2).map((g, i) => (
            <span key={i} className="fc-pill fc-pill-genre">{g}</span>
          ))}
          {item.instrumental != null && (
            <span className="fc-pill fc-pill-voice">
              {item.instrumental > 0.5 ? "Instrumental" : "Vocal"}
            </span>
          )}
          {item.moods && Object.entries(item.moods)
            .filter(([,v]) => v != null && v > 0.6).sort(([,a],[,b]) => b-a).slice(0,1)
            .map(([k]) => <span key={k} className="fc-pill fc-pill-mood">{k.toUpperCase()}</span>)
          }
          {!item.onnxDone && (
            <span className="fc-pill fc-pill-analyzing">Descubriendo estilo…</span>
          )}
        </div>
      )}

      {item.status === "error" && (
        <p className="fc-error-msg">{item.error || "Error de conversión"}</p>
      )}
    </div>
  );
}

// ── Converter ─────────────────────────────────────────────────────────────────
function Converter() {
  const [items,   setItems]   = useState([]);
  const [format,  setFormat]  = useState("mp3");
  const [quality, setQuality] = useState(320);
  const [phase,   setPhase]   = useState("idle");
  const [hot,     setHot]     = useState(false);
  const [dlMode,  setDlMode]  = useState(null);
  const [audioOpts,     setAudioOpts]     = useState({ trimSilence: false, silenceThreshold: -60, fadeIn: false, fadeInDuration: 2, fadeOut: false, fadeOutDuration: 2 });
  const [showAudioOpts, setShowAudioOpts] = useState(false);
  const [zipLoading,    setZipLoading]    = useState(false);

  const inputRef    = useRef(null);
  const queueRef    = useRef(null);
  const itemsRef    = useRef([]);
  const savedIdsRef = useRef(new Set());

  useEffect(() => { window.LibraryStore?.init?.().catch(() => {}); }, []);
  useEffect(() => { itemsRef.current = items; }, [items]);

  const currentFmt = OUTPUT_FORMATS.find(f => f.id === format) || OUTPUT_FORMATS[0];

  const doneItems    = items.filter(i => i.status === "done");
  const errorItems   = items.filter(i => i.status === "error");
  const pendingItems = items.filter(i => i.status === "pending");
  const isConverting = phase === "converting";
  const allOnnxDone  = doneItems.length > 0 && doneItems.every(i => i.onnxDone);

  // Update a single item by localId (merge patch)
  const patchItem = useCallback((localId, patch) => {
    setItems(prev => prev.map(i => i.localId === localId ? { ...i, ...patch } : i));
  }, []);

  // Save to library once ONNX analysis is done (success or failure).
  // This guarantees genre/style data is present when the user navigates to the library.
  useEffect(() => {
    if (!window.LibraryStore) return;
    for (const item of items) {
      if (item.status !== "done" || !item.onnxDone) continue;
      if (savedIdsRef.current.has(item.localId)) continue;
      savedIdsRef.current.add(item.localId);
      const _fmtObj = OUTPUT_FORMATS.find(f => f.id === format);
      window.LibraryStore.addTrack({
        id:           item.localId,
        name:         item.file.name.replace(/\.[^.]+$/, ""),
        filename:     item.file.name.replace(/\.[^.]+$/, "") + "." + (_fmtObj?.ext || format),
        format,
        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,
      });
    }
  }, [items]);

  // Detect when all active jobs are settled
  useEffect(() => {
    if (phase !== "converting") return;
    const cur = itemsRef.current;
    const active = cur.filter(i => i.status === "queued" || i.status === "converting");
    if (!active.length && cur.length) setPhase("done");
  }, [items, phase]);

  // Destroy worker pool on unmount
  useEffect(() => () => queueRef.current?.destroy(), []);

  // Warn before leaving if conversion is in progress
  useEffect(() => {
    const handler = (e) => {
      if (phase === "converting") { e.preventDefault(); e.returnValue = ''; }
    };
    window.addEventListener('beforeunload', handler);
    return () => window.removeEventListener('beforeunload', handler);
  }, [phase]);

  // ── File handling ───────────────────────────────────────────────────────────
  const makeItem = (f) => {
    const ext = getExt(f.name);
    const ok  = SUPPORTED.includes(ext);
    const big = f.size > MAX_BYTES;
    return {
      localId: uid(), file: f, status: !ok ? "error" : big ? "error" : "pending",
      error: !ok ? "Formato no compatible" : big ? `Demasiado grande (${fmtBytes(f.size)})` : null,
      progress: 0, position: 0,
      bpm: null, key: null, camelot: null, genres: null,
      danceability: null, instrumental: null, moods: null, onnxDone: false,
    };
  };

  const addFiles = useCallback((fileList) => {
    const arr = Array.from(fileList);
    setItems(prev => {
      const space = MAX_FILES - prev.length;
      if (space <= 0) return prev;
      const usedBytes = prev.reduce((s, i) => s + (i.file?.size || 0), 0);
      const candidates = arr.slice(0, space);
      let running = usedBytes;
      return [...prev, ...candidates.map(f => {
        const item = makeItem(f);
        if (!item.error) {
          running += f.size;
          if (running > MAX_TOTAL_BYTES) {
            item.status = "error";
            item.error  = "Límite total 2 GB superado";
          }
        }
        return item;
      })];
    });
    setPhase(p => p === "idle" ? "selected" : p);
  }, []);

  const removeItem = (localId) => {
    setItems(prev => {
      const next = prev.filter(i => i.localId !== localId);
      if (!next.length) setPhase("idle");
      return next;
    });
  };

  const onDrop = (e) => {
    e.preventDefault(); setHot(false);
    if (isConverting) return;
    addFiles(e.dataTransfer.files);
  };
  const onPick = (e) => { if (e.target.files) addFiles(e.target.files); e.target.value = ""; };

  const handleFormatChange = (fmtId) => {
    const f = OUTPUT_FORMATS.find(x => x.id === fmtId);
    if (!f) return;
    setFormat(fmtId);
    setQuality(f.lossless ? null : f.kbps[f.kbps.length - 1]);
  };

  // ── Conversion (client-side) ────────────────────────────────────────────────
  const startConversion = () => {
    const toConvert = items.filter(i => i.status === "pending");
    if (!toConvert.length) return;
    setPhase("converting");

    // Create worker pool if not already running
    if (!queueRef.current) {
      queueRef.current = new ClientQueue((localId, patch) => {
        setItems(prev => prev.map(i => i.localId === localId ? { ...i, ...patch } : i));
      });
    }

    for (const item of toConvert) {
      queueRef.current.enqueue(item.localId, item.file, format, quality || 320, audioOpts);
    }
  };

  // ── Download helpers ────────────────────────────────────────────────────────
  const downloadOne = (item) => {
    const blob = queueRef.current?.getBlob(item.localId);
    if (!blob) return;
    const a = document.createElement("a");
    const baseName = item.file.name.replace(/\.[^.]+$/, "");
    a.href     = blob.url;
    a.download = `${baseName}.${blob.ext}`;
    a.rel      = "noopener";
    document.body.appendChild(a);
    a.click();
    setTimeout(() => document.body.removeChild(a), 250);
  };

  const downloadZip = async () => {
    if (!doneItems.length || !queueRef.current || zipLoading) return;
    setZipLoading(true);
    try {
      if (window.JSZip) {
        const zip  = new JSZip();
        const seen = new Set();
        for (const item of doneItems) {
          const blob = queueRef.current.getBlob(item.localId);
          if (!blob) continue;
          const baseName = item.file.name.replace(/\.[^.]+$/, "");
          let name = `${baseName}.${blob.ext}`;
          let n = 1;
          while (seen.has(name)) name = `${baseName} (${n++}).${blob.ext}`;
          seen.add(name);
          const buf = await blob.blob.arrayBuffer();
          zip.file(name, buf);
        }
        const content = await zip.generateAsync({ type: "blob", compression: "STORE" });
        const a = document.createElement("a");
        a.href     = URL.createObjectURL(content);
        a.download = "antonic-converted.zip";
        a.rel      = "noopener";
        document.body.appendChild(a);
        a.click();
        setTimeout(() => { URL.revokeObjectURL(a.href); document.body.removeChild(a); }, 1000);
      } else {
        for (const item of doneItems) downloadOne(item);
      }
    } finally {
      setZipLoading(false);
    }
  };

  const reset = () => {
    queueRef.current?.destroy();
    queueRef.current = null;
    setItems([]); setPhase("idle"); setDlMode(null);
    savedIdsRef.current = new Set();
  };

  // ── Render ─────────────────────────────────────────────────────────────────
  return (
    <div className="converter" id="converter">

      {/* Chrome */}
      <div className="converter-chrome">
        <div className="chrome-left">
          <span className="chrome-led">
            <i className={phase !== "idle" ? "on" : ""} />
            <i className={["selected","converting","done"].includes(phase) ? "on" : ""} />
            <i className={["converting","done"].includes(phase) ? "on" : ""} />
            <i className={phase === "done" ? "on" : ""} />
          </span>
          <span>Convertidor · Master</span>
        </div>
        <div className="chrome-right">
          <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, letterSpacing: ".12em" }}>
            {phase === "idle"       && "READY"}
            {phase === "selected"   && `${pendingItems.length} ARCHIVO${pendingItems.length !== 1 ? "S" : ""} · LISTO`}
            {phase === "converting" && `CONVIRTIENDO · ${doneItems.length + errorItems.length}/${items.length}`}
            {phase === "done"       && `DONE · ${doneItems.length}/${items.length}`}
          </span>
        </div>
      </div>

      {/* Dropzone */}
      {phase === "idle" && (
        <div
          className={"dropzone" + (hot ? " hot" : "")}
          onDragOver={e => { e.preventDefault(); setHot(true); }}
          onDragEnter={e => { e.preventDefault(); setHot(true); }}
          onDragLeave={() => setHot(false)}
          onDrop={onDrop}
        >
          <div className="drop-icon">
            <svg width="34" height="34" viewBox="0 0 24 24" fill="none">
              <path d="M12 3v13M7 12l5 5 5-5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
              <path d="M4 19h16" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
            </svg>
          </div>
          <h3 className="drop-title">Arrastra tus archivos aquí</h3>
          <p className="drop-sub">Hasta <b>{MAX_FILES} archivos</b> · máx. 500 MB por archivo · 2 GB total</p>
          <div className="drop-actions">
            <button className="btn btn-primary" onClick={() => inputRef.current?.click()} style={{ height: 44, fontSize: 14, padding: "0 18px" }}>
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 4v16M4 12h16" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"/></svg>
              Seleccionar archivos
            </button>
            <span className="drop-or">o suelta aquí · hasta {MAX_FILES} archivos de golpe</span>
          </div>
          <input ref={inputRef} type="file" hidden multiple onChange={onPick}
            accept=".mp3,.alac,.aac,.m4a,.mp4,.wav,.aiff,.aif,.flac,.ape,.tta,.wv,.ogg,.opus,.wma,.dsf,.dff,.caf" />
          <div className="formats-mini">
            {["mp3","wav","aiff","flac","alac","aac","ogg","opus","wma","ape","wv","dsf","dff","caf"].map(f => <span key={f}>.{f}</span>)}
          </div>
        </div>
      )}

      {/* File grid */}
      {phase !== "idle" && (
        <>
          <div className="batch-header">
            <span className="batch-count">{items.length} archivo{items.length !== 1 ? "s" : ""}</span>
            {phase === "selected" && items.length < MAX_FILES && (
              <button className="batch-add" onClick={() => inputRef.current?.click()}>+ añadir más</button>
            )}
          </div>
          {phase === "selected" && (
            <input ref={inputRef} type="file" hidden multiple onChange={onPick}
              accept=".mp3,.alac,.aac,.m4a,.mp4,.wav,.aiff,.aif,.flac,.ape,.tta,.wv,.ogg,.opus,.wma,.dsf,.dff,.caf" />
          )}
          <div className="file-grid">
            {items.map(item => (
              <FileCard key={item.localId} item={item} onRemove={removeItem} locked={isConverting} />
            ))}
          </div>
        </>
      )}

      {/* Format + quality selector */}
      {(phase === "selected" || phase === "converting") && (
        <div style={{ marginTop: 26 }}>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 12 }}>
            <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, letterSpacing: ".16em", textTransform: "uppercase", color: "var(--text-3)" }}>Formato de salida</span>
          </div>
          <div className="fmt-selector">
            {OUTPUT_FORMATS.map(f => (
              <button key={f.id} className={"fmt-pill" + (format === f.id ? " active" : "")}
                disabled={isConverting} onClick={() => handleFormatChange(f.id)} title={f.sub}>
                <span className="fmt-pill-label">{f.label}</span>
                <span className="fmt-pill-sub">{f.lossless ? "lossless" : f.sub}</span>
              </button>
            ))}
          </div>

          {!currentFmt.lossless && (
            <div style={{ marginTop: 20 }}>
              <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
                <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, letterSpacing: ".16em", textTransform: "uppercase", color: "var(--text-3)" }}>Calidad de salida</span>
                <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--text-3)", letterSpacing: ".08em" }}>{currentFmt.label} · {quality} kbps</span>
              </div>
              <div className={"quality-grid cols-" + currentFmt.kbps.length} style={{ marginTop: 12 }}>
                {currentFmt.kbps.map((kbps, i) => {
                  const ql    = QUALITY_LABELS[kbps] || { label: kbps + " kbps", sub: "" };
                  const isTop = kbps === currentFmt.kbps[currentFmt.kbps.length - 1];
                  return (
                    <button key={kbps} className={"q-card" + (quality === kbps ? " active" : "")}
                      disabled={isConverting} onClick={() => setQuality(kbps)}>
                      {isTop && <span className="q-tag">Recomendado</span>}
                      <div className="q-kbps">{kbps}<span>kbps</span></div>
                      <div className="q-label">{ql.label}</div>
                      <div className="q-bars">
                        {[0,1,2,3,4].map(n => (
                          <i key={n} style={{ height: `${(i+1) >= n+1 ? Math.min(100,(n+1)*22) : 18}%` }} />
                        ))}
                      </div>
                    </button>
                  );
                })}
              </div>
            </div>
          )}

          {currentFmt.lossless && (
            <div className="lossless-info">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" stroke="currentColor" strokeWidth="2" strokeLinejoin="round"/></svg>
              <div><b>{currentFmt.label} sin pérdida</b> — calidad idéntica al original.</div>
            </div>
          )}
        </div>
      )}

      {/* Audio processing options */}
      {(phase === "selected" || phase === "converting") && (
        <div style={{ marginTop: 16 }}>
          <button
            style={{ background:'none', border:'1px solid var(--line-2)', borderRadius:7, color: showAudioOpts ? 'var(--text-1)' : 'var(--text-3)', padding:'5px 12px', cursor:'pointer', fontSize:11, fontFamily:'var(--font-mono)', letterSpacing:'.1em', textTransform:'uppercase', display:'flex', alignItems:'center', gap:6, opacity: isConverting ? 0.5 : 1 }}
            disabled={isConverting}
            onClick={() => setShowAudioOpts(p => !p)}
          >
            <svg width="10" height="10" viewBox="0 0 24 24" fill="none"><path d={showAudioOpts ? "M6 9l6 6 6-6" : "M9 18l6-6-6-6"} stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
            Opciones de audio
            {(audioOpts.trimSilence || audioOpts.fadeIn || audioOpts.fadeOut) && (
              <span style={{ background:'var(--accent)', borderRadius:4, fontSize:9, padding:'1px 5px', color:'#000', fontWeight:700, letterSpacing:0 }}>ACTIVAS</span>
            )}
          </button>

          {showAudioOpts && (
            <div style={{ marginTop:10, padding:'14px 16px', background:'var(--ink-2)', border:'1px solid var(--line-2)', borderRadius:10, display:'flex', flexDirection:'column', gap:10 }}>
              <label style={{ display:'flex', alignItems:'center', gap:8, cursor:'pointer', fontSize:13 }}>
                <input type="checkbox" checked={audioOpts.trimSilence} disabled={isConverting}
                  onChange={e => setAudioOpts(p => ({...p, trimSilence: e.target.checked}))} />
                Eliminar silencios
                {audioOpts.trimSilence && (
                  <span style={{ display:'flex', alignItems:'center', gap:5, marginLeft:4 }}>
                    <span style={{ fontSize:11, color:'var(--text-3)' }}>umbral</span>
                    <input type="number" value={audioOpts.silenceThreshold} min="-80" max="-20" disabled={isConverting}
                      onChange={e => setAudioOpts(p => ({...p, silenceThreshold: Number(e.target.value)}))}
                      style={{ width:52, padding:'2px 6px', borderRadius:5, border:'1px solid var(--line-2)', background:'var(--ink-3)', color:'var(--text-1)', fontSize:12, fontFamily:'var(--font-mono)' }} />
                    <span style={{ fontSize:11, color:'var(--text-3)' }}>dB</span>
                  </span>
                )}
              </label>
              <label style={{ display:'flex', alignItems:'center', gap:8, cursor:'pointer', fontSize:13 }}>
                <input type="checkbox" checked={audioOpts.fadeIn} disabled={isConverting}
                  onChange={e => setAudioOpts(p => ({...p, fadeIn: e.target.checked}))} />
                Fade in
                {audioOpts.fadeIn && (
                  <span style={{ display:'flex', alignItems:'center', gap:5, marginLeft:4 }}>
                    <input type="number" value={audioOpts.fadeInDuration} min="0.1" max="30" step="0.5" disabled={isConverting}
                      onChange={e => setAudioOpts(p => ({...p, fadeInDuration: Math.max(0.1, Number(e.target.value))}))}
                      style={{ width:44, padding:'2px 6px', borderRadius:5, border:'1px solid var(--line-2)', background:'var(--ink-3)', color:'var(--text-1)', fontSize:12, fontFamily:'var(--font-mono)' }} />
                    <span style={{ fontSize:11, color:'var(--text-3)' }}>s</span>
                  </span>
                )}
              </label>
              <label style={{ display:'flex', alignItems:'center', gap:8, cursor:'pointer', fontSize:13 }}>
                <input type="checkbox" checked={audioOpts.fadeOut} disabled={isConverting}
                  onChange={e => setAudioOpts(p => ({...p, fadeOut: e.target.checked}))} />
                Fade out
                {audioOpts.fadeOut && (
                  <span style={{ display:'flex', alignItems:'center', gap:5, marginLeft:4 }}>
                    <input type="number" value={audioOpts.fadeOutDuration} min="0.1" max="30" step="0.5" disabled={isConverting}
                      onChange={e => setAudioOpts(p => ({...p, fadeOutDuration: Math.max(0.1, Number(e.target.value))}))}
                      style={{ width:44, padding:'2px 6px', borderRadius:5, border:'1px solid var(--line-2)', background:'var(--ink-3)', color:'var(--text-1)', fontSize:12, fontFamily:'var(--font-mono)' }} />
                    <span style={{ fontSize:11, color:'var(--text-3)' }}>s</span>
                  </span>
                )}
              </label>
            </div>
          )}
        </div>
      )}

      {/* Convert button */}
      {phase === "selected" && (
        <div className="convert-row" style={{ marginTop: 24 }}>
          <div className="convert-info">
            <b>{pendingItems.length} archivo{pendingItems.length !== 1 ? "s" : ""}</b> → {currentFmt.label}{!currentFmt.lossless ? ` ${quality} kbps` : " lossless"}
          </div>
          <button className="btn-convert" onClick={startConversion} disabled={!pendingItems.length}>
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M6 4l14 8-14 8V4z" fill="currentColor"/></svg>
            Convertir {pendingItems.length} archivo{pendingItems.length !== 1 ? "s" : ""}
          </button>
        </div>
      )}

      {/* Done: descarga + reset */}
      {phase === "done" && (
        <div style={{ marginTop: 28 }}>
          {doneItems.length === 1 && (
            <button className="btn-zip" onClick={() => downloadOne(doneItems[0])}>
              <svg width="20" height="20" 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>
              Descargar archivo
            </button>
          )}

          {doneItems.length > 1 && dlMode === null && (
            <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 10 }}>
              <p style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--text-3)", letterSpacing: ".1em", textTransform: "uppercase", margin: 0 }}>
                {doneItems.length} archivos listos — ¿cómo descargar?
              </p>
              <div style={{ display: "flex", gap: 10, justifyContent: "center", flexWrap: "wrap" }}>
                <button className="btn-zip" onClick={downloadZip} disabled={zipLoading} style={{ opacity: zipLoading ? 0.7 : 1 }}>
                  <svg width="20" height="20" 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>
                  {zipLoading ? "Preparando ZIP…" : "ZIP · todo junto"}
                </button>
                <button className="btn btn-ghost" style={{ height: 50, padding: "0 22px" }}
                  onClick={() => setDlMode("separate")}>
                  Archivos por separado
                </button>
              </div>
            </div>
          )}

          {doneItems.length > 1 && dlMode === "separate" && (
            <div style={{ display: "flex", flexDirection: "column", gap: 6, alignItems: "stretch" }}>
              <p style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--text-3)", textAlign: "center", margin: "0 0 6px", letterSpacing: ".1em", textTransform: "uppercase" }}>
                Haz clic en cada archivo
              </p>
              {doneItems.map(item => {
                const blob = queueRef.current?.getBlob(item.localId);
                const ext  = blob?.ext || currentFmt.ext;
                const name = item.file.name.replace(/\.[^.]+$/, "") + "." + ext;
                return (
                  <button key={item.localId} onClick={() => downloadOne(item)}
                    style={{ display: "flex", alignItems: "center", gap: 8, padding: "10px 14px",
                             background: "rgba(255,255,255,0.04)", border: "1px solid var(--line-2)",
                             borderRadius: 8, color: "var(--text-1)", cursor: "pointer",
                             fontSize: 13, fontFamily: "var(--font-mono)" }}>
                    <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>
                    {name}
                  </button>
                );
              })}
            </div>
          )}

          {errorItems.length > 0 && (
            <p style={{ textAlign: "center", color: "var(--text-3)", fontSize: 13, margin: "12px 0 0" }}>
              {errorItems.length} archivo{errorItems.length !== 1 ? "s" : ""} no {errorItems.length !== 1 ? "se pudieron convertir" : "se pudo convertir"}
            </p>
          )}
          <div style={{ display: "flex", justifyContent: "center", alignItems: "center", gap: 14, marginTop: 16, flexWrap: "wrap" }}>
            <button className="btn btn-ghost" onClick={reset}>Convertir otro lote</button>
            {doneItems.length > 0 && (
              allOnnxDone
                ? <a href="playlist.html" style={{ fontFamily: "var(--font-mono)", fontSize: 11,
                    color: "var(--text-3)", letterSpacing: ".08em", textDecoration: "none" }}>
                    Guardado en biblioteca →
                  </a>
                : <span style={{ fontFamily: "var(--font-mono)", fontSize: 11,
                    color: "var(--text-3)", letterSpacing: ".08em", opacity: 0.5 }}>
                    Analizando géneros…
                  </span>
            )}
          </div>
        </div>
      )}

    </div>
  );
}

Object.assign(window, { Converter });
