// Lacunas idiomáticas — app principal
// Tres vistas: cosmos (palabras flotando), índice (lista editorial), detalle (ficha).
// + Buscador en vivo + Propuesta de concepto (localStorage)

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

const LS_KEY = "lacunas.propuestas.v1";
const LS_SUMMONED = "lacunas.summoned.v2";  // versión nueva: arranca vacío

const BATCH_SIZE = 7; // palabras por chorro

// ─────────────────────────────────────────────────────────────────────────────
// Hash determinista para semilla
// ─────────────────────────────────────────────────────────────────────────────
function hash(s) {
  let h = 2166136261 >>> 0;
  for (let i = 0; i < s.length; i++) {
    h ^= s.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  return h >>> 0;
}

// Normaliza texto (sin tildes, minúsculas) para búsqueda
function norm(s) {
  return (s || "")
    .toString()
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase();
}

// ─────────────────────────────────────────────────────────────────────────────
// Físicas del cosmos — usa Map por id para preservar posiciones al añadir/quitar
// ─────────────────────────────────────────────────────────────────────────────
function useFloatingPositions(items, intensity, containerRef, freshIds) {
  const stateRef = useRef(new Map());
  const [, force] = useState(0);
  const rafRef = useRef(0);

  // Sincroniza el mapa de estado con la lista actual de items.
  useEffect(() => {
    let cancelled = false;
    const sync = () => {
      if (cancelled) return;
      const el = containerRef.current;
      const w = (el && el.clientWidth) || window.innerWidth || 1200;
      const h = (el && el.clientHeight) || (window.innerHeight - 200) || 600;
      if (w < 200 || h < 200) {
        requestAnimationFrame(sync);
        return;
      }
      const speedScale = intensity === "calmo" ? 0.25 : intensity === "intenso" ? 1.1 : 0.55;
      const map = stateRef.current;
      const validIds = new Set(items.map(i => i.id));

      // Elimina los que ya no están
      for (const id of [...map.keys()]) {
        if (!validIds.has(id)) map.delete(id);
      }

      // Añade los nuevos en una posición sensata
      const newOnes = items.filter(it => !map.has(it.id));
      const existing = items.length - newOnes.length;
      const cols = Math.max(2, Math.round(Math.sqrt(items.length * w / Math.max(h, 1))));

      newOnes.forEach((it, idx) => {
        const seed = hash(it.id);
        const r1 = ((seed % 1000) / 1000);
        const r2 = (((seed * 9301 + 49297) % 233280) / 233280);
        const r3 = (((seed * 1103515245 + 12345) % 2147483648) / 2147483648);
        const r4 = (((seed * 22695477 + 1) % 2147483648) / 2147483648);
        const isFresh = freshIds && freshIds.has(it.id);

        let x, y, vx, vy;
        if (isFresh) {
          // Surtidor: las palabras brotan desde el botón (bottom-center).
          // Velocidad inicial escalada al alto del contenedor para que
          // siempre lleguen hasta la mitad superior.
          x = w / 2 + (r1 - 0.5) * 8;
          y = h + 10;
          const angle = -Math.PI / 2 + (r2 - 0.5) * Math.PI * 0.45; // mayormente recto hacia arriba
          const speed = Math.max(5, h * 0.009) + r3 * 3;
          vx = Math.cos(angle) * speed;
          vy = Math.sin(angle) * speed;
        } else {
          // Disposición inicial en rejilla — restringida a la mitad superior
          const halfH = h * 0.55;
          const totalIdx = existing + idx;
          const col = totalIdx % cols;
          const row = Math.floor(totalIdx / cols);
          const cellW = w / cols;
          const cellH = halfH / Math.max(1, Math.ceil(items.length / cols));
          x = (col + 0.5) * cellW + (r1 - 0.5) * cellW * 0.5;
          y = (row + 0.5) * cellH + (r2 - 0.5) * cellH * 0.5;
          x = Math.max(120, Math.min(w - 120, x));
          y = Math.max(60, Math.min(halfH - 20, y));
          vx = (r3 - 0.5) * 0.35 * speedScale;
          vy = (r4 - 0.5) * 0.35 * speedScale;
        }

        map.set(it.id, {
          x, y, vx, vy,
          rot: (r1 - 0.5) * 4,
          rotV: (r2 - 0.5) * 0.03 * speedScale,
          phase: r3 * Math.PI * 2,
          radius: 70 + (it.palabra.length * 5),
          birth: performance.now(),
          fresh: !!isFresh,
          // Solo las palabras frescas necesitan ser "dispersadas" tras asentarse;
          // las que ya estaban (carga inicial) llegan con su propia velocidad.
          dispersed: !isFresh,
        });
      });

      force(x => x + 1);
    };
    sync();
    return () => { cancelled = true; };
  }, [items, intensity]);

  useEffect(() => {
    let last = performance.now();
    const tick = (now) => {
      const dt = Math.min(50, now - last);
      last = now;
      const el = containerRef.current;
      if (!el) { rafRef.current = requestAnimationFrame(tick); return; }
      const w = el.clientWidth;
      const h = el.clientHeight;
      const speedScale = intensity === "calmo" ? 0.25 : intensity === "intenso" ? 1.1 : 0.55;

      const map = stateRef.current;
      const arr = [...map.values()];

      for (const p of arr) {
        p.x += p.vx * dt * 0.06;
        p.y += p.vy * dt * 0.06;
        p.rot += p.rotV * dt * 0.06;
        // Después de 2.5s, asentar definitivamente
        if (p.fresh && (now - p.birth) > 2500) p.fresh = false;
        const px = Math.min(w * 0.42, p.radius * 0.9);
        const pyTop = Math.min(h * 0.30, 50);
        const peakY = h * 0.55;     // altura de "techo" de la fuente
        const floorY = h - 50;       // suelo del cosmos asentado
        if (!p.fresh) {
          // Asentada: rebote en TODOS los bordes (cosmos completo)
          if (p.x < px) { p.x = px; p.vx = Math.abs(p.vx); }
          if (p.x > w - px) { p.x = w - px; p.vx = -Math.abs(p.vx); }
          if (p.y < pyTop) { p.y = pyTop; p.vy = Math.abs(p.vy); }
          if (p.y > floorY) { p.y = floorY; p.vy = -Math.abs(p.vy); }
        } else {
          // Fresca: rebote solo en laterales y techo. El "suelo del surtidor"
          // es la altura del pico — cuando la palabra cruza hacia arriba,
          // queda liberada y empieza a flotar por todo el cosmos.
          if (p.x < px) { p.x = px; p.vx = Math.abs(p.vx); }
          if (p.x > w - px) { p.x = w - px; p.vx = -Math.abs(p.vx); }
          if (p.y < pyTop) { p.y = pyTop; p.vy = Math.abs(p.vy); }
          if (p.y < peakY) p.fresh = false;
        }
        // Cuando se acaba de asentar, le damos una dirección aleatoria
        // para que se disperse por todo el cosmos en vez de quedarse quieta.
        if (!p.fresh && !p.dispersed) {
          const ang = Math.random() * Math.PI * 2;
          const mvSettled = 0.9 * speedScale;
          p.vx = Math.cos(ang) * mvSettled;
          p.vy = Math.sin(ang) * mvSettled;
          p.dispersed = true;
        }
        p.vx += Math.sin((now / 4000 + p.phase)) * 0.0008 * speedScale * dt;
        p.vy += Math.cos((now / 5200 + p.phase)) * 0.0008 * speedScale * dt;
        // Velocidad máxima:
        //  - "fresca": muy alta para que el surtidor las propulse.
        //  - asentada: un drift cómodo que las mantiene viajando por el cosmos.
        const mv = p.fresh ? 12 : 0.9 * speedScale;
        p.vx = Math.max(-mv, Math.min(mv, p.vx));
        p.vy = Math.max(-mv, Math.min(mv, p.vy));
        // Si está fresca, fricción para frenar suavemente al alcanzar el pico
        if (p.fresh) {
          p.vx *= 0.992;
          p.vy *= 0.992;
        }
      }
      // Repulsión
      for (let i = 0; i < arr.length; i++) {
        for (let j = i + 1; j < arr.length; j++) {
          const a = arr[i], b = arr[j];
          const dx = b.x - a.x;
          const dy = b.y - a.y;
          const dist2 = dx * dx + dy * dy;
          const minD = (a.radius + b.radius) * 0.45;
          if (dist2 < minD * minD && dist2 > 0.01) {
            const dist = Math.sqrt(dist2);
            const overlap = (minD - dist) / minD;
            const f = 0.04 * overlap * speedScale;
            const nx = dx / dist;
            const ny = dy / dist;
            a.vx -= nx * f;
            a.vy -= ny * f;
            b.vx += nx * f;
            b.vy += ny * f;
          }
        }
      }
      force(n => (n + 1) % 1000000);
      rafRef.current = requestAnimationFrame(tick);
    };
    rafRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafRef.current);
  }, [intensity]);

  return stateRef;
}

// ─────────────────────────────────────────────────────────────────────────────
// Partículas de fondo
// ─────────────────────────────────────────────────────────────────────────────
function ParticleField({ intensity }) {
  const canvasRef = useRef(null);
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    let raf, w, h, dpr;
    const resize = () => {
      dpr = Math.min(2, window.devicePixelRatio || 1);
      w = canvas.clientWidth;
      h = canvas.clientHeight;
      canvas.width = w * dpr;
      canvas.height = h * dpr;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };
    resize();
    window.addEventListener("resize", resize);

    const count = intensity === "calmo" ? 70 : intensity === "intenso" ? 220 : 130;
    const speed = intensity === "calmo" ? 0.05 : intensity === "intenso" ? 0.25 : 0.12;
    const particles = Array.from({ length: count }, () => ({
      x: Math.random() * w,
      y: Math.random() * h,
      r: Math.random() * 1.4 + 0.2,
      vx: (Math.random() - 0.5) * speed,
      vy: (Math.random() - 0.5) * speed,
      a: Math.random() * 0.5 + 0.15,
      pulse: Math.random() * Math.PI * 2,
    }));

    const draw = (t) => {
      ctx.clearRect(0, 0, w, h);
      for (const p of particles) {
        p.x += p.vx;
        p.y += p.vy;
        if (p.x < 0) p.x = w;
        if (p.x > w) p.x = 0;
        if (p.y < 0) p.y = h;
        if (p.y > h) p.y = 0;
        const flick = 0.5 + 0.5 * Math.sin(t / 800 + p.pulse);
        const a = p.a * (0.4 + 0.6 * flick);
        const fill = `rgba(40,38,32,${a * 0.55})`;
        ctx.beginPath();
        ctx.fillStyle = fill;
        ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
        ctx.fill();
      }
      raf = requestAnimationFrame(draw);
    };
    raf = requestAnimationFrame(draw);
    return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", resize); };
  }, [intensity]);

  return <canvas ref={canvasRef} className="lac-particles" />;
}

// ─────────────────────────────────────────────────────────────────────────────
// Vista cosmos — el tamaño de cada palabra se adapta al total visible
// ─────────────────────────────────────────────────────────────────────────────
function Cosmos({ items, onPick, intensity, hoveredId, setHoveredId, freshIds, archivoVacio }) {
  const containerRef = useRef(null);
  const stateRef = useFloatingPositions(items, intensity, containerRef, freshIds);
  const isNarrow = typeof window !== "undefined" && window.innerWidth < 720;

  // Escalado dinámico: más palabras → más pequeñas
  const n = items.length;
  const scaleFactor = n <= 12 ? 1.0
                    : n <= 24 ? 0.85
                    : n <= 40 ? 0.72
                    : n <= 60 ? 0.60
                    : n <= 80 ? 0.52
                    : 0.46;

  const sizeFor = (w) => {
    const len = w.palabra.length;
    let s;
    if (len <= 4)       s = 44;
    else if (len <= 7)  s = 56;
    else if (len <= 10) s = 64;
    else if (len <= 14) s = 52;
    else                s = 40;
    s = s * scaleFactor;
    return Math.round(isNarrow ? s * 0.7 : s);
  };

  return (
    <div ref={containerRef} className="lac-cosmos">
      {items.map((it) => {
        const p = stateRef.current.get(it.id) || { x: -200, y: -200, rot: 0 };
        const isHover = hoveredId === it.id;
        const isFresh = freshIds && freshIds.has(it.id);
        const size = sizeFor(it);
        return (
          <button
            key={it.id}
            className={`lac-word ${isHover ? "is-hover" : ""} ${it.propuesta ? "is-proposal" : ""} ${isFresh ? "is-fresh" : ""}`}
            style={{
              transform: `translate3d(${p.x}px, ${p.y}px, 0) rotate(${p.rot}deg) translate(-50%, -50%)`,
              fontSize: `${size}px`,
            }}
            onMouseEnter={() => setHoveredId(it.id)}
            onMouseLeave={() => setHoveredId(null)}
            onClick={() => onPick(it)}
          >
            <span className="lac-word-text">{it.palabra}</span>
            <span className="lac-word-meta">
              <span className="lac-word-lang">{it.idioma.toLowerCase()}</span>
              <span className="lac-word-cat">· {it.categoria}</span>
              {it.propuesta && <span className="lac-word-proposal">· propuesta</span>}
            </span>
          </button>
        );
      })}
      {n === 0 && (
        archivoVacio ? (
          <div className="lac-empty lac-empty-source">
            <span className="lac-empty-eyebrow">archivo en silencio</span>
            <p className="lac-empty-poem">
              ninguna palabra<br/>
              ha sido <em>invocada</em><br/>
              todavía.
            </p>
            <span className="lac-empty-arrow">↓</span>
          </div>
        ) : (
          <div className="lac-empty">
            <p>sin resultados.<br/><span className="lac-empty-cta">¿propones tú una?</span></p>
          </div>
        )
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// Vista índice
// ─────────────────────────────────────────────────────────────────────────────
function Indice({ items, onPick, query }) {
  return (
    <div className="lac-indice">
      <div className="lac-indice-head">
        <span>nº</span>
        <span>palabra</span>
        <span>lengua</span>
        <span>categoría</span>
        <span>breve</span>
      </div>
      {items.length === 0 && (
        <div className="lac-empty-indice">
          {query ? <>sin resultados para «{query}».</> : <>sin entradas.</>}
        </div>
      )}
      {items.map((it, i) => (
        <button
          key={it.id}
          className={`lac-indice-row ${it.propuesta ? "is-proposal" : ""}`}
          onClick={() => onPick(it)}>
          <span className="lac-indice-num">{String(i + 1).padStart(2, "0")}</span>
          <span className="lac-indice-word">
            {it.palabra}
            {it.propuesta && <span className="lac-indice-pmark">⁕</span>}
          </span>
          <span className="lac-indice-lang">{it.idioma}</span>
          <span className="lac-indice-cat">{it.categoria}</span>
          <span className="lac-indice-breve">{it.breve}</span>
        </button>
      ))}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// Vista detalle
// ─────────────────────────────────────────────────────────────────────────────
function Detalle({ item, onClose, onPrev, onNext, onDeleteProposal }) {
  useEffect(() => {
    const k = (e) => {
      if (e.key === "Escape") onClose();
      if (e.key === "ArrowLeft") onPrev();
      if (e.key === "ArrowRight") onNext();
    };
    window.addEventListener("keydown", k);
    return () => window.removeEventListener("keydown", k);
  }, [onClose, onPrev, onNext]);

  if (!item) return null;
  return (
    <div className="lac-detail-scrim" onClick={onClose}>
      <article
        className={`lac-detail ${item.propuesta ? "is-proposal" : ""}`}
        onClick={(e) => e.stopPropagation()}>
        <header className="lac-detail-head">
          <div className="lac-detail-meta">
            {item.propuesta && <span className="lac-pill lac-pill-prop">propuesta</span>}
            <span className="lac-pill">{item.idioma}</span>
            <span className="lac-dot">·</span>
            <span className="lac-mono">{item.region}</span>
            {item.año && item.año !== "—" && <>
              <span className="lac-dot">·</span>
              <span className="lac-mono">{item.año}</span>
            </>}
          </div>
          <button className="lac-x" onClick={onClose} aria-label="cerrar">×</button>
        </header>

        <h1 className="lac-detail-word">
          <span>{item.palabra}</span>
        </h1>

        <div className="lac-detail-phon">
          {item.fonetica && <span className="lac-mono">/ {item.fonetica} /</span>}
          <span className="lac-cat-tag">· {item.categoria}</span>
        </div>

        <p className="lac-detail-breve">{item.breve}</p>

        <div className="lac-detail-rule" />

        {item.largo && <p className="lac-detail-largo">{item.largo}</p>}

        {item.perifrasis && (
          <div className="lac-detail-perifrasis">
            <span className="lac-detail-label">perífrasis al castellano</span>
            <p className="lac-detail-perif-text">{item.perifrasis}</p>
          </div>
        )}

        {item.uso && item.uso !== "—" && (
          <div className="lac-detail-uso">
            <span className="lac-detail-label">en uso</span>
            <p className="lac-detail-uso-text">«{item.uso}»</p>
          </div>
        )}

        {item.propuesta && (
          <div className="lac-detail-prop-info">
            <span className="lac-detail-label">propuesta guardada</span>
            <p className="lac-mono lac-prop-meta">
              {item.autor ? `— ${item.autor}` : "— anónimo"}
              {item.creadoEn ? ` · ${new Date(item.creadoEn).toLocaleDateString("es-ES")}` : ""}
            </p>
            <button className="lac-link-btn" onClick={() => onDeleteProposal(item.id)}>
              eliminar de mi archivo
            </button>
          </div>
        )}

        <footer className="lac-detail-foot">
          <button className="lac-nav-btn" onClick={onPrev}>← anterior</button>
          <span className="lac-detail-foot-id">
            lacuna nº {String(item._index + 1).padStart(2, "0")} de {item._total}
          </span>
          <button className="lac-nav-btn" onClick={onNext}>siguiente →</button>
        </footer>
      </article>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// Modal de propuesta
// ─────────────────────────────────────────────────────────────────────────────
function ProponerModal({ open, onClose, onSave, categorias }) {
  const [palabra, setPalabra] = useState("");
  const [idioma, setIdioma] = useState("");
  const [region, setRegion] = useState("");
  const [categoria, setCategoria] = useState("asombro");
  const [breve, setBreve] = useState("");
  const [largo, setLargo] = useState("");
  const [perifrasis, setPerifrasis] = useState("");
  const [autor, setAutor] = useState("");
  const [done, setDone] = useState(false);

  useEffect(() => {
    if (open) {
      setPalabra(""); setIdioma(""); setRegion("");
      setCategoria("asombro"); setBreve(""); setLargo("");
      setPerifrasis(""); setAutor(""); setDone(false);
    }
  }, [open]);

  useEffect(() => {
    if (!open) return;
    const k = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", k);
    return () => window.removeEventListener("keydown", k);
  }, [open, onClose]);

  if (!open) return null;

  const submit = (e) => {
    e.preventDefault();
    if (!palabra.trim() || !idioma.trim() || !breve.trim()) return;
    const entry = {
      id: "prop-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 6),
      palabra: palabra.trim(),
      idioma: idioma.trim(),
      region: region.trim() || "—",
      fonetica: "",
      categoria: categoria,
      breve: breve.trim(),
      largo: largo.trim(),
      perifrasis: perifrasis.trim(),
      autor: autor.trim(),
      creadoEn: Date.now(),
      propuesta: true,
    };
    onSave(entry);
    setDone(true);
    // Enviar al archivo del estudio (fire-and-forget)
    fetch("https://www.unbancodeideas.com/api/lacuna-propuesta", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        palabra: entry.palabra,
        idioma: entry.idioma,
        region: entry.region,
        categoria: entry.categoria,
        breve: entry.breve,
        largo: entry.largo,
        perifrasis: entry.perifrasis,
        autor: entry.autor,
      }),
    }).catch(() => {}); // silencioso si falla — la propuesta ya quedó en localStorage
  };

  return (
    <div className="lac-detail-scrim" onClick={onClose}>
      <div className="lac-detail lac-modal" onClick={(e) => e.stopPropagation()}>
        <header className="lac-detail-head">
          <div className="lac-detail-meta">
            <span className="lac-pill lac-pill-prop">proponer</span>
            <span className="lac-mono">una nueva lacuna</span>
          </div>
          <button className="lac-x" onClick={onClose} aria-label="cerrar">×</button>
        </header>

        {done ? (
          <div className="lac-prop-done">
            <h2 className="lac-prop-done-title"><span>gracias.</span></h2>
            <p className="lac-prop-done-text">
              Tu propuesta queda guardada en tu archivo personal y aparece marcada con
              <span className="lac-mark"> ⁕ </span> entre las demás lacunas.
            </p>
            <p className="lac-prop-done-text lac-prop-done-fine">
              Para enviarla al estudio, exporta tus propuestas desde el pie de página
              y compártelas con nosotros.
            </p>
            <div className="lac-prop-actions">
              <button className="lac-nav-btn" onClick={onClose}>cerrar</button>
            </div>
          </div>
        ) : (
          <form className="lac-form" onSubmit={submit}>
            <p className="lac-form-intro">
              Si conoces una palabra que el castellano no tiene y debería tener, déjala aquí.
              Esta colección está viva — se nutre de quienes la leen.
            </p>

            <div className="lac-form-row">
              <label className="lac-form-field">
                <span className="lac-form-lbl">palabra <i>· obligatorio</i></span>
                <input
                  type="text"
                  value={palabra}
                  onChange={(e) => setPalabra(e.target.value)}
                  placeholder="ej. Komorebi"
                  required
                />
              </label>
              <label className="lac-form-field">
                <span className="lac-form-lbl">lengua <i>· obligatorio</i></span>
                <input
                  type="text"
                  value={idioma}
                  onChange={(e) => setIdioma(e.target.value)}
                  placeholder="ej. Japonés"
                  required
                />
              </label>
            </div>

            <div className="lac-form-row">
              <label className="lac-form-field">
                <span className="lac-form-lbl">región</span>
                <input
                  type="text"
                  value={region}
                  onChange={(e) => setRegion(e.target.value)}
                  placeholder="ej. Japón"
                />
              </label>
              <label className="lac-form-field">
                <span className="lac-form-lbl">categoría</span>
                <select value={categoria} onChange={(e) => setCategoria(e.target.value)}>
                  {categorias.filter(c => c.id !== "todas").map(c => (
                    <option key={c.id} value={c.id}>{c.etiqueta}</option>
                  ))}
                </select>
              </label>
            </div>

            <label className="lac-form-field">
              <span className="lac-form-lbl">definición breve <i>· obligatorio</i></span>
              <input
                type="text"
                value={breve}
                onChange={(e) => setBreve(e.target.value)}
                placeholder="una sola línea, en castellano"
                maxLength={200}
                required
              />
            </label>

            <label className="lac-form-field">
              <span className="lac-form-lbl">ensayo / contexto</span>
              <textarea
                value={largo}
                onChange={(e) => setLargo(e.target.value)}
                placeholder="por qué importa, de dónde viene, qué dice del mundo..."
                rows={4}
              />
            </label>

            <label className="lac-form-field">
              <span className="lac-form-lbl">perífrasis al castellano</span>
              <input
                type="text"
                value={perifrasis}
                onChange={(e) => setPerifrasis(e.target.value)}
                placeholder="la frase más cercana en castellano"
              />
            </label>

            <label className="lac-form-field">
              <span className="lac-form-lbl">firma <i>· opcional</i></span>
              <input
                type="text"
                value={autor}
                onChange={(e) => setAutor(e.target.value)}
                placeholder="tu nombre o un seudónimo"
              />
            </label>

            <div className="lac-form-actions">
              <button type="button" className="lac-nav-btn" onClick={onClose}>cancelar</button>
              <button type="submit" className="lac-submit-btn">guardar propuesta</button>
            </div>
          </form>
        )}
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// App
// ─────────────────────────────────────────────────────────────────────────────

const LAC_DEFAULTS = /*EDITMODE-BEGIN*/{
  "intensidad": "vivo",
  "vista": "cosmos",
  "categoria": "todas",
  "mostrarMetaSiempre": false
}/*EDITMODE-END*/;

function App() {
  const [t, setTweak] = useTweaks(LAC_DEFAULTS);
  const [selectedId, setSelectedId] = useState(null);
  const [hoveredId, setHoveredId] = useState(null);
  const [query, setQuery] = useState("");
  const [proponerOpen, setProponerOpen] = useState(false);
  const [propuestas, setPropuestas] = useState(() => {
    try {
      const raw = localStorage.getItem(LS_KEY);
      return raw ? JSON.parse(raw) : [];
    } catch { return []; }
  });
  // IDs de palabras invocadas desde la Fuente (persisten entre sesiones).
  // El archivo arranca en silencio: nada visible salvo lo que el usuario invoque.
  const [summonedIds, setSummonedIds] = useState(() => {
    try {
      const raw = localStorage.getItem(LS_SUMMONED);
      return raw ? new Set(JSON.parse(raw)) : new Set();
    } catch { return new Set(); }
  });
  // IDs recién añadidos en esta sesión (para animación de entrada)
  const [freshIds, setFreshIds] = useState(new Set());

  // Persistir propuestas y palabras invocadas
  useEffect(() => {
    try { localStorage.setItem(LS_KEY, JSON.stringify(propuestas)); } catch {}
  }, [propuestas]);
  useEffect(() => {
    try { localStorage.setItem(LS_SUMMONED, JSON.stringify([...summonedIds])); } catch {}
  }, [summonedIds]);

  // Pozo completo (curadas + extras) — todas se invocan desde la misma Fuente
  const pozo = useMemo(
    () => [...(window.LACUNAS || []), ...(window.LACUNAS_EXTRAS || [])],
    []
  );

  // Lista visible = invocadas del pozo + propuestas del usuario
  const todas = useMemo(() => {
    const visibles = pozo.filter(e => summonedIds.has(e.id));
    return [...visibles, ...propuestas];
  }, [pozo, propuestas, summonedIds]);

  // Cuántas quedan en el pozo por invocar
  const disponibles = useMemo(() => {
    return pozo.filter(e => !summonedIds.has(e.id));
  }, [pozo, summonedIds]);

  // Invocar el siguiente chorro desde la Fuente — palabras al azar,
  // inyectadas una a una desde el botón.
  const invocar = useCallback(() => {
    if (disponibles.length === 0) return;
    const shuffled = [...disponibles];
    for (let i = shuffled.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
    }
    const batch = shuffled.slice(0, BATCH_SIZE);

    batch.forEach((entry, i) => {
      setTimeout(() => {
        setSummonedIds(prev => {
          const next = new Set(prev);
          next.add(entry.id);
          return next;
        });
        setFreshIds(prev => {
          const next = new Set(prev);
          next.add(entry.id);
          return next;
        });
        setTimeout(() => {
          setFreshIds(prev => {
            const next = new Set(prev);
            next.delete(entry.id);
            return next;
          });
        }, 4500);
      }, i * 170);
    });
  }, [disponibles]);

  // Filtrado: categoría + búsqueda
  const filtered = useMemo(() => {
    const q = norm(query.trim());
    return todas.filter(w => {
      if (t.categoria !== "todas" && w.categoria !== t.categoria) return false;
      if (!q) return true;
      return (
        norm(w.palabra).includes(q) ||
        norm(w.idioma).includes(q) ||
        norm(w.region).includes(q) ||
        norm(w.breve).includes(q) ||
        norm(w.largo).includes(q) ||
        norm(w.categoria).includes(q) ||
        norm(w.perifrasis).includes(q)
      );
    });
  }, [todas, t.categoria, query]);

  const selected = useMemo(() => {
    if (!selectedId) return null;
    const idx = todas.findIndex(w => w.id === selectedId);
    if (idx === -1) return null;
    return { ...todas[idx], _index: idx, _total: todas.length };
  }, [selectedId, todas]);

  const onPrev = () => {
    const idx = todas.findIndex(w => w.id === selectedId);
    const nxt = (idx - 1 + todas.length) % todas.length;
    setSelectedId(todas[nxt].id);
  };
  const onNext = () => {
    const idx = todas.findIndex(w => w.id === selectedId);
    const nxt = (idx + 1) % todas.length;
    setSelectedId(todas[nxt].id);
  };

  const onSavePropuesta = (entry) => {
    setPropuestas(prev => [...prev, entry]);
  };
  const onDeletePropuesta = (id) => {
    setPropuestas(prev => prev.filter(p => p.id !== id));
    setSelectedId(null);
  };

  const exportarPropuestas = () => {
    if (propuestas.length === 0) {
      setProponerOpen(true);
      return;
    }
    const lines = propuestas.map(p =>
`╶─ ${p.palabra} (${p.idioma}) ─╴
categoría: ${p.categoria}
región: ${p.region || "—"}
breve: ${p.breve}
${p.largo ? `ensayo: ${p.largo}\n` : ""}${p.perifrasis ? `perífrasis: ${p.perifrasis}\n` : ""}${p.autor ? `firma: ${p.autor}\n` : ""}—\n`
    ).join("\n");
    const header = `Propuestas para Lacunas — ${new Date().toLocaleString("es-ES")}\n${"─".repeat(48)}\n\n`;
    const text = header + lines;
    navigator.clipboard?.writeText(text).then(
      () => alert("Tus propuestas se han copiado al portapapeles.\nPégalas en un correo a estudioprompt y pasarán a revisión."),
      () => {
        const w = window.open("", "_blank");
        if (w) { w.document.body.innerText = text; }
      }
    );
  };

  const [now, setNow] = useState(() => new Date());
  useEffect(() => {
    const i = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(i);
  }, []);
  const hhmm = now.toLocaleTimeString("es-ES", { hour: "2-digit", minute: "2-digit" });

  return (
    <div className={`lac-root ${t.mostrarMetaSiempre ? "show-meta" : ""}`}>
      <ParticleField intensity={t.intensidad} />

      <header className="lac-header">
        <div className="lac-header-left">
          <div className="lac-logo">
            <span className="lac-logo-mark">◐</span>
            <span className="lac-logo-text">Lacunas</span>
          </div>
          <span className="lac-tag">archivo de palabras que el castellano no tiene</span>
        </div>
        <div className="lac-header-right">
          <div className="lac-search">
            <span className="lac-search-icon">⌕</span>
            <input
              className="lac-search-input"
              type="text"
              value={query}
              onChange={(e) => setQuery(e.target.value)}
              placeholder="buscar palabra, lengua, concepto..."
            />
            {query && (
              <button className="lac-search-clear" onClick={() => setQuery("")} aria-label="limpiar">×</button>
            )}
          </div>
          <button className="lac-propose-btn" onClick={() => setProponerOpen(true)}>
            <span className="lac-propose-plus">+</span>
            <span className="lac-propose-label">proponer</span>
          </button>
        </div>
      </header>

      <nav className="lac-nav">
        <div className="lac-views">
          <button
            className={`lac-view-btn ${t.vista === "cosmos" ? "is-active" : ""}`}
            onClick={() => setTweak("vista", "cosmos")}>◯ cosmos</button>
          <button
            className={`lac-view-btn ${t.vista === "indice" ? "is-active" : ""}`}
            onClick={() => setTweak("vista", "indice")}>☰ índice</button>
        </div>
        <div className="lac-cats">
          {window.CATEGORIAS.map(c => {
            const cnt = c.id === "todas"
              ? todas.length
              : todas.filter(w => w.categoria === c.id).length;
            if (cnt === 0 && c.id !== "todas") return null;
            return (
              <button
                key={c.id}
                className={`lac-cat-btn ${t.categoria === c.id ? "is-active" : ""}`}
                onClick={() => setTweak("categoria", c.id)}>
                {c.etiqueta}
                {c.id !== "todas" && <span className="lac-cat-count">{cnt}</span>}
              </button>
            );
          })}
        </div>
        <div className="lac-counter">
          <span className="lac-mono">{String(filtered.length).padStart(2, "0")}</span>
          <span className="lac-counter-of">/ {String(todas.length).padStart(2, "0")}</span>
          <span className="lac-counter-clock lac-mono">· {hhmm}</span>
        </div>
      </nav>

      <button
        className={`lac-convocar ${disponibles.length === 0 ? "is-done" : ""} ${todas.length === 0 ? "is-prominent" : ""}`}
        onClick={invocar}
        disabled={disponibles.length === 0}
        title="invoca una nueva oleada de palabras">
        <span className="lac-convocar-icon">✦</span>
        <span className="lac-convocar-text">
          {disponibles.length > 0 ? "Fuente Lacuna" : "fuente seca"}
        </span>
      </button>

      <main className="lac-main">
        {t.vista === "cosmos" ? (
          <Cosmos
            items={filtered}
            onPick={(it) => setSelectedId(it.id)}
            intensity={t.intensidad}
            hoveredId={hoveredId}
            setHoveredId={setHoveredId}
            freshIds={freshIds}
            archivoVacio={todas.length === 0 && !query && t.categoria === "todas"}
          />
        ) : (
          <Indice items={filtered} onPick={(it) => setSelectedId(it.id)} query={query} />
        )}
      </main>

      <footer className="lac-foot">
        <span className="lac-mono">est. mmxxvi</span>
        <span className="lac-foot-quote">
          «los límites de mi lenguaje significan los límites de mi mundo» <em>— wittgenstein</em>
        </span>
        <span className="lac-foot-actions">
          {propuestas.length > 0 && (
            <button className="lac-link-btn" onClick={exportarPropuestas}>
              exportar mis propuestas ({propuestas.length})
            </button>
          )}
          <span className="lac-mono">estudioprompt.com</span>
        </span>
      </footer>

      <Detalle
        item={selected}
        onClose={() => setSelectedId(null)}
        onPrev={onPrev}
        onNext={onNext}
        onDeleteProposal={onDeletePropuesta}
      />

      <ProponerModal
        open={proponerOpen}
        onClose={() => setProponerOpen(false)}
        onSave={onSavePropuesta}
        categorias={window.CATEGORIAS}
      />

      <TweaksPanel>
        <TweakSection label="Movimiento" />
        <TweakRadio
          label="Intensidad"
          value={t.intensidad}
          options={["calmo", "vivo", "intenso"]}
          onChange={(v) => setTweak("intensidad", v)}
        />
        <TweakSection label="Apariencia" />
        <TweakToggle
          label="Metadatos siempre visibles"
          value={t.mostrarMetaSiempre}
          onChange={(v) => setTweak("mostrarMetaSiempre", v)}
        />
        <TweakSection label="Vista" />
        <TweakRadio
          label="Modo"
          value={t.vista}
          options={["cosmos", "indice"]}
          onChange={(v) => setTweak("vista", v)}
        />
      </TweaksPanel>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
