/* =========================================================
   La casita — sala privada de dos.

   Modelo de seguridad
   -------------------
   · El código de sala solo ubica la sala y abre el "directorio"
     (los nombres y las claves públicas). No abre la conversación.
   · Cada persona tiene su propia identidad: un par ECDH (para
     cifrar) y un par ECDSA (para firmar). Las privadas se guardan
     cifradas con SU frase personal, que la otra no conoce.
   · La clave de la conversación sale de ECDH entre los dos. Nadie
     más puede derivarla, ni con el código de sala en la mano.
   · Cada mensaje va firmado. Si la firma no cuadra con la clave
     pública de quien dice ser, sale marcado como no verificado.
   · La sala solo admite dos personas. La tercera no entra.

   Para añadir un juego: mira el objeto JUEGOS al final.
   ========================================================= */

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

/* ---------------- temas ---------------- */
const TEMAS = {
  normal: {
    fondo:"#0E0F16", aura1:"rgba(232,85,126,.16)", aura2:"rgba(127,214,169,.10)",
    panel:"#1A1C27", panel2:"#232633", line:"#3A3F52", ink:"#EDEBE6", dim:"#9AA0B4",
    hot:"#E8557E", gold:"#F2D492", ok:"#7FD6A9", glow:"rgba(242,212,146,.55)",
  },
  picante: {
    fondo:"#150409", aura1:"rgba(255,45,110,.28)", aura2:"rgba(150,20,200,.20)",
    panel:"#26060F", panel2:"#360A17", line:"#5C1B2E", ink:"#FFE9EF", dim:"#C98A9E",
    hot:"#FF2D6E", gold:"#FFB3C8", ok:"#FF7AA8", glow:"rgba(255,45,110,.7)",
  },
};

const norm = (s) => s.normalize("NFD").replace(/[\u0300-\u036f]/g,"").toUpperCase();
const hora = (t) => new Date(t).toLocaleTimeString("es-CO", { hour:"2-digit", minute:"2-digit" });

/* ---------------- primitivas ---------------- */
const te = new TextEncoder(), td = new TextDecoder();
const b64 = (b) => btoa(String.fromCharCode(...new Uint8Array(b)));
const unb64 = (s) => Uint8Array.from(atob(s), c => c.charCodeAt(0));
const P256 = { namedCurve: "P-256" };

async function pbkdf2(secreto, salt, iter, algo) {
  const base = await crypto.subtle.importKey("raw", te.encode(secreto), "PBKDF2", false, ["deriveBits","deriveKey"]);
  return crypto.subtle.deriveKey({ name:"PBKDF2", salt, iterations:iter, hash:"SHA-256" },
                                 base, algo, false, ["encrypt","decrypt"]);
}
async function cifrar(clave, obj) {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const ct = await crypto.subtle.encrypt({ name:"AES-GCM", iv }, clave, te.encode(JSON.stringify(obj)));
  return JSON.stringify({ v:2, iv:b64(iv), ct:b64(ct) });
}
async function descifrar(clave, crudo) {
  const s = JSON.parse(crudo);
  const plano = await crypto.subtle.decrypt({ name:"AES-GCM", iv:unb64(s.iv) }, clave, unb64(s.ct));
  return JSON.parse(td.decode(plano));
}

/* ---- sala: id + clave del directorio (a partir del código) ---- */
async function derivarSala(codigo) {
  const base = await crypto.subtle.importKey("raw", te.encode(codigo), "PBKDF2", false, ["deriveBits","deriveKey"]);
  const sobre = await crypto.subtle.deriveKey(
    { name:"PBKDF2", salt: te.encode("casita.v2.directorio"), iterations:150000, hash:"SHA-256" },
    base, { name:"AES-GCM", length:256 }, false, ["encrypt","decrypt"]);
  const bits = await crypto.subtle.deriveBits(
    { name:"PBKDF2", salt: te.encode("casita.v2.id"), iterations:50000, hash:"SHA-256" }, base, 128);
  const id = [...new Uint8Array(bits)].map(b => b.toString(16).padStart(2,"0")).join("");
  return { id, sobre };
}

/* ---- identidad personal ---- */
const limpiaJwk = (j) => { const c = { ...j }; delete c.key_ops; delete c.ext; return c; };
const impDHpub  = (j) => crypto.subtle.importKey("jwk", limpiaJwk(j), { name:"ECDH", ...P256 }, true, []);
const impDHpriv = (j) => crypto.subtle.importKey("jwk", limpiaJwk(j), { name:"ECDH", ...P256 }, true, ["deriveKey"]);
const impSGpub  = (j) => crypto.subtle.importKey("jwk", limpiaJwk(j), { name:"ECDSA", ...P256 }, true, ["verify"]);
const impSGpriv = (j) => crypto.subtle.importKey("jwk", limpiaJwk(j), { name:"ECDSA", ...P256 }, true, ["sign"]);

async function crearIdentidad(frase) {
  const dh = await crypto.subtle.generateKey({ name:"ECDH", ...P256 }, true, ["deriveKey"]);
  const sg = await crypto.subtle.generateKey({ name:"ECDSA", ...P256 }, true, ["sign","verify"]);
  const priv = {
    dh: await crypto.subtle.exportKey("jwk", dh.privateKey),
    sg: await crypto.subtle.exportKey("jwk", sg.privateKey),
  };
  const pub = {
    dh: await crypto.subtle.exportKey("jwk", dh.publicKey),
    sg: await crypto.subtle.exportKey("jwk", sg.publicKey),
  };
  const salt = crypto.getRandomValues(new Uint8Array(16));
  const clave = await pbkdf2(frase, salt, 200000, { name:"AES-GCM", length:256 });
  return {
    ranura: { pub, salt: b64(salt), priv: await cifrar(clave, priv) },
    mias: { dh: dh.privateKey, sg: sg.privateKey },
  };
}

async function abrirIdentidad(ranura, frase) {
  const clave = await pbkdf2(frase, unb64(ranura.salt), 200000, { name:"AES-GCM", length:256 });
  const priv = await descifrar(clave, ranura.priv);   // lanza si la frase no es
  return { dh: await impDHpriv(priv.dh), sg: await impSGpriv(priv.sg) };
}

async function claveConversacion(miDHpriv, suDHpubJwk) {
  return crypto.subtle.deriveKey(
    { name:"ECDH", public: await impDHpub(suDHpubJwk) },
    miDHpriv, { name:"AES-GCM", length:256 }, false, ["encrypt","decrypt"]);
}

async function firmar(privSG, contenido) {
  const f = await crypto.subtle.sign({ name:"ECDSA", hash:"SHA-256" }, privSG,
                                     te.encode(JSON.stringify(contenido)));
  return b64(f);
}
async function verificar(pubSGJwk, contenido, firma) {
  try {
    return await crypto.subtle.verify({ name:"ECDSA", hash:"SHA-256" },
      await impSGpub(pubSGJwk), unb64(firma), te.encode(JSON.stringify(contenido)));
  } catch (e) { return false; }
}

/* ---------------- sonido ---------------- */
let ctxAudio = null;
function tono(f, dur=.14, tipo="sine", vol=.06, retraso=0) {
  try {
    ctxAudio = ctxAudio || new (window.AudioContext || window.webkitAudioContext)();
    const t = ctxAudio.currentTime + retraso;
    const o = ctxAudio.createOscillator(), g = ctxAudio.createGain();
    o.type = tipo; o.frequency.setValueAtTime(f, t);
    g.gain.setValueAtTime(vol, t);
    g.gain.exponentialRampToValueAtTime(.0001, t + dur);
    o.connect(g).connect(ctxAudio.destination);
    o.start(t); o.stop(t + dur);
  } catch (e) {}
}
const SONIDO = {
  acierto: () => tono(680,.11,"triangle"),
  fallo: () => tono(140,.24,"sawtooth",.05),
  gana: () => [523,659,784,1046].forEach((f,i) => tono(f,.2,"triangle",.06,i*.1)),
  pierde: () => [330,262,196].forEach((f,i) => tono(f,.36,"sine",.06,i*.16)),
  mensaje: () => { tono(880,.07,"sine",.045); tono(1170,.09,"sine",.04,.07); },
  clic: () => tono(520,.07,"triangle",.04),
};

const estadoInicial = () => ({ juego:"ahorcado", picante:false, marcador:{}, datos:{}, ts:0 });

/* =========================================================
   App
   ========================================================= */
function App() {
  const [sesion, setSesion] = useState(null);   // { nombre, id, sobre, mias, directorio }
  const [estado, setEstado] = useState(estadoInicial());
  const [mensajes, setMensajes] = useState([]);
  const [reacciones, setReacciones] = useState({});   // { firmaDelMensaje: { autor: emoji } }
  const [escribiendo, setEscribiendo] = useState(null);
  const [directorio, setDirectorio] = useState(null);
  const [conectado, setConectado] = useState(false);
  const [vista, setVista] = useState("chat");
  const [sinLeer, setSinLeer] = useState(0);
  const [sonido, setSonido] = useState(true);
  const [angosto, setAngosto] = useState(window.innerWidth < 900);

  const convRef = useRef(null);          // clave AES de la conversación
  const vRef = useRef(-1), mRef = useRef(0), dRef = useRef(-1), vivoRef = useRef(false);
  const eRef = useRef(0);                      // cursor del aviso "esta escribiendo"
  const escTimer = useRef(null), ultimoAviso = useRef(0);
  const estadoRef = useRef(estado), sonidoRef = useRef(sonido), dirRef = useRef(null);
  estadoRef.current = estado; sonidoRef.current = sonido; dirRef.current = directorio;

  const T = TEMAS[estado.picante ? "picante" : "normal"];
  const nombres = directorio ? Object.keys(directorio.ranuras) : [];
  const otro = sesion ? nombres.find(n => n !== sesion.nombre) : null;
  const listo = Boolean(convRef.current);

  useEffect(() => {
    const f = () => setAngosto(window.innerWidth < 900);
    window.addEventListener("resize", f);
    return () => window.removeEventListener("resize", f);
  }, []);
  useEffect(() => () => { vivoRef.current = false; }, []);

  /* ---- emparejar cuando aparezca la otra persona ---- */
  const emparejar = useCallback(async (dir, ses) => {
    const suNombre = Object.keys(dir.ranuras).find(n => n !== ses.nombre);
    if (!suNombre || convRef.current) return;
    try {
      convRef.current = await claveConversacion(ses.mias.dh, dir.ranuras[suNombre].pub.dh);
      setEstado(e => ({ ...e }));   // repinta
    } catch (e) {}
  }, []);

  /* ---- publicar estado de juego ---- */
  const publicar = useCallback(async (nuevo) => {
    if (!convRef.current) return;
    const con = { ...nuevo, ts: Date.now() };
    setEstado(con);
    try {
      await fetch(`/api/estado?id=${sesion.id}`, { method:"POST",
        headers:{ "Content-Type":"text/plain" }, body: await cifrar(convRef.current, con) });
    } catch (e) { setConectado(false); }
  }, [sesion]);

  /* ---- enviar mensaje firmado ---- */
  // Devuelve true solo si el servidor lo recibio, para que el chat no borre
  // del campo un mensaje que en realidad no salio.
  const enviar = useCallback(async (texto) => {
    const limpio = texto.trim();
    if (!limpio || !convRef.current) return false;
    const c = { autor: sesion.nombre, texto: limpio.slice(0,800), t: Date.now() };
    const paquete = { c, f: await firmar(sesion.mias.sg, c) };
    try {
      const r = await fetch(`/api/mensaje?id=${sesion.id}`, { method:"POST",
        headers:{ "Content-Type":"text/plain" }, body: await cifrar(convRef.current, paquete) });
      if (!r.ok) throw new Error("envio");
      return true;
    } catch (e) { setConectado(false); return false; }
  }, [sesion]);

  /* ---- reaccionar a un mensaje ----
     'ref' es la FIRMA del mensaje: ya es única y ya viene en los mensajes que
     estaban guardados antes, así que se puede reaccionar al historial viejo
     sin cambiarle nada al formato. La reacción también va firmada: una que no
     verifique se descarta al recibirla. */
  const reaccionar = useCallback(async (ref, emoji, quitar) => {
    if (!convRef.current || !ref) return;
    const c = { tipo:"reaccion", ref, emoji, quitar: !!quitar,
                autor: sesion.nombre, t: Date.now() };
    const paquete = { c, f: await firmar(sesion.mias.sg, c) };
    try {
      await fetch(`/api/mensaje?id=${sesion.id}`, { method:"POST",
        headers:{ "Content-Type":"text/plain" }, body: await cifrar(convRef.current, paquete) });
    } catch (e) { setConectado(false); }
  }, [sesion]);

  /* ---- "está escribiendo" ----
     Va por su propio endpoint, que el servidor guarda solo en memoria. Como
     mucho un aviso cada 3 s: ni satura la Pi ni gasta el límite de peticiones. */
  const avisarEscribiendo = useCallback(async () => {
    if (!convRef.current || !sesion) return;
    const ahora = Date.now();
    if (ahora - ultimoAviso.current < 3000) return;
    ultimoAviso.current = ahora;
    try {
      await fetch(`/api/escribiendo?id=${sesion.id}`, { method:"POST",
        headers:{ "Content-Type":"text/plain" },
        body: await cifrar(convRef.current, { autor: sesion.nombre, t: ahora }) });
    } catch (e) {}
  }, [sesion]);

  /* ---- bucle de sincronización ---- */
  const arrancarBucle = useCallback((ses) => {
    vivoRef.current = true;
    (async function bucle() {
      while (vivoRef.current) {
        try {
          const r = await fetch(
            `/api/sync?id=${ses.id}&v=${vRef.current}&m=${mRef.current}&d=${dRef.current}` +
            `&e=${eRef.current}&espera=25`,
            { cache:"no-store" });
          if (!r.ok) throw new Error("sync");
          const d = await r.json();
          setConectado(true);
          vRef.current = d.version;

          if (d.directorio) {
            dRef.current = d.dir_version;
            try {
              const dir = await descifrar(ses.sobre, d.directorio);
              setDirectorio(dir);
              await emparejar(dir, ses);
            } catch (e) {}
          }
          if (d.estado && convRef.current) {
            try {
              const remoto = await descifrar(convRef.current, d.estado);
              if (remoto.ts >= (estadoRef.current.ts || 0)) setEstado(remoto);
            } catch (e) {}
          }
          if (d.mensajes?.length && convRef.current) {
            const abiertos = [], reacs = [];
            for (const crudo of d.mensajes) {
              try {
                const p = await descifrar(convRef.current, crudo);
                const dir = dirRef.current;
                const pub = dir?.ranuras[p.c.autor]?.pub.sg;
                const ok = pub ? await verificar(pub, p.c, p.f) : false;
                if (p.c.tipo === "reaccion") {
                  if (ok) reacs.push(p.c);        // sin firma buena, no cuenta
                } else if (!p.c.tipo) {
                  // los mensajes de siempre: mismo formato de antes, mas su id
                  abiertos.push({ ...p.c, ok, id: p.f });
                }
                // cualquier otro 'tipo' se ignora en silencio, para no pintar
                // basura si algun dia se agrega algo nuevo
              } catch (e) {}
            }
            if (abiertos.length) {
              setMensajes(prev => [...prev, ...abiertos]);
              const ajenos = abiertos.filter(m => m.autor !== ses.nombre).length;
              if (ajenos) {
                if (sonidoRef.current) SONIDO.mensaje();
                setSinLeer(n => n + ajenos);
                setEscribiendo(null);            // ya escribio: se apaga el aviso
              }
            }
            if (reacs.length) {
              setReacciones(prev => {
                const n = { ...prev };
                for (const r of reacs) {
                  const m = { ...(n[r.ref] || {}) };
                  if (r.quitar) delete m[r.autor]; else m[r.autor] = r.emoji;
                  if (Object.keys(m).length) n[r.ref] = m; else delete n[r.ref];
                }
                return n;
              });
            }
          }

          if (typeof d.esc === "number") eRef.current = d.esc;
          if (d.escribiendo && convRef.current) {
            try {
              const q = await descifrar(convRef.current, d.escribiendo);
              if (q.autor && q.autor !== ses.nombre) {
                setEscribiendo(q.autor);
                clearTimeout(escTimer.current);
                escTimer.current = setTimeout(() => setEscribiendo(null), 5000);
              }
            } catch (e) {}
          }
          if (typeof d.total === "number") {
            if (d.total < mRef.current) { setMensajes([]); setReacciones({}); }   // borraron el chat
            mRef.current = d.total;
          }
        } catch (e) {
          setConectado(false);
          await new Promise(r => setTimeout(r, 2500));
        }
      }
    })();
  }, [emparejar]);

  const entrar = useCallback(async (ses, dir) => {
    setSesion(ses);
    setDirectorio(dir);
    dirRef.current = dir;
    await emparejar(dir, ses);
    arrancarBucle(ses);
  }, [arrancarBucle, emparejar]);

  const salir = () => {
    vivoRef.current = false;
    try { localStorage.removeItem("casita:recordado"); } catch (e) {}
    location.reload();
  };

  const borrarChat = async () => {
    if (!confirm("¿Borrar todo el chat para los dos? No se puede deshacer.")) return;
    try {
      await fetch(`/api/mensajes?id=${sesion.id}`, { method:"DELETE" });
      setMensajes([]);
      setReacciones({});
    } catch (e) {}
  };

  useEffect(() => { if (vista === "chat" || !angosto) setSinLeer(0); },
            [vista, angosto, mensajes.length]);

  if (!sesion) return <Entrada onEntrar={entrar} />;

  const juego = JUEGOS[estado.juego] || JUEGOS.ahorcado;
  const ctx = { estado, publicar, yo: sesion.nombre, T, sonido, SONIDO };

  const panelJuego = (
    <div style={{ padding: angosto ? "16px 14px 90px" : "22px 26px", overflowY:"auto", height:"100%" }}>
      {!listo ? (
        <Esperando T={T} nombre={sesion.nombre} />
      ) : (
        <>
          <div style={{ display:"flex", flexWrap:"wrap", gap:8, marginBottom:20, alignItems:"center" }}>
            {Object.entries(JUEGOS).map(([k, j]) => (
              <button key={k}
                onClick={() => { if (sonido) SONIDO.clic();
                  publicar({ ...estado, juego:k,
                             datos:{ ...estado.datos, [k]: estado.datos[k] || j.inicial() } }); }}
                style={{ background: estado.juego === k ? T.hot : T.panel,
                         color: estado.juego === k ? "#14060C" : T.dim,
                         border:`1px solid ${estado.juego === k ? T.hot : T.line}`,
                         borderRadius:99, padding:"7px 15px", fontSize:13, fontWeight:700,
                         cursor:"pointer",
                         boxShadow: estado.juego === k ? `0 0 20px ${T.glow}` : "none" }}>
                {j.nombre}
              </button>
            ))}
            <div style={{ flex:1 }} />
            <button onClick={() => publicar({ ...estado, picante: !estado.picante })}
              style={{ background: estado.picante ? T.hot : "transparent",
                       color: estado.picante ? "#1A0209" : T.dim,
                       border:`1px solid ${estado.picante ? T.hot : T.line}`,
                       borderRadius:99, padding:"7px 14px", fontSize:12, fontWeight:700, cursor:"pointer" }}>
              modo picante
            </button>
          </div>
          <Marcador estado={estado} T={T} />
          <juego.Vista {...ctx} />
        </>
      )}
    </div>
  );

  const panelChat = (
    <Chat mensajes={mensajes} yo={sesion.nombre} otro={otro} T={T} enviar={enviar}
          conectado={conectado} angosto={angosto} listo={listo}
          sonido={sonido} setSonido={setSonido} salir={salir} borrarChat={borrarChat}
          reacciones={reacciones} reaccionar={reaccionar}
          escribiendo={escribiendo} avisarEscribiendo={avisarEscribiendo} />
  );

  return (
    <div style={{ background:T.fondo, color:T.ink, height:"100dvh", position:"relative",
                  overflow:"hidden", transition:"background .6s ease" }}>
      {/* Las auras van en su propia caja recortada: si cuelgan del div raiz,
          su desborde lo vuelve scrolleable y scrollIntoView termina moviendo
          toda la interfaz (el encabezado del chat se iba de pantalla). */}
      <div style={{ position:"absolute", inset:0, overflow:"hidden", pointerEvents:"none" }}>
        <div className="aura" style={{ width:520, height:520, background:T.aura1, top:-160, left:-120 }} />
        <div className="aura" style={{ width:460, height:460, background:T.aura2, bottom:-180, right:-100, animationDelay:"5s" }} />
      </div>

      {angosto ? (
        <div style={{ height:"100%", position:"relative" }}>
          {vista === "juego" ? panelJuego : <div style={{ height:"100%" }}>{panelChat}</div>}
          <nav style={{ position:"fixed", left:0, right:0, bottom:0, display:"flex", gap:8, padding:10,
                        background:`${T.panel}ee`, borderTop:`1px solid ${T.line}`, backdropFilter:"blur(10px)" }}>
            {[["chat","Chat"],["juego","Juegos"]].map(([k,txt]) => (
              <button key={k} onClick={() => setVista(k)}
                style={{ flex:1, padding:"12px 0", borderRadius:12, fontWeight:700, fontSize:14, cursor:"pointer",
                         background: vista === k ? T.hot : "transparent",
                         color: vista === k ? "#14060C" : T.dim,
                         border:`1px solid ${vista === k ? T.hot : T.line}`, position:"relative" }}>
                {txt}
                {k === "chat" && sinLeer > 0 && vista !== "chat" && (
                  <span style={{ position:"absolute", top:6, right:"26%", background:T.ok, color:"#0C1F16",
                                 borderRadius:99, fontSize:11, padding:"1px 7px" }}>{sinLeer}</span>
                )}
              </button>
            ))}
          </nav>
        </div>
      ) : (
        <div style={{ display:"grid", gridTemplateColumns:"1fr 360px", height:"100%", position:"relative" }}>
          {panelJuego}
          {/* height 100% explicito: sin el, la columna queda con altura auto, el
              Chat crece hasta el alto de la conversacion y desborda el div raiz. */}
          <div style={{ borderLeft:`1px solid ${T.line}`, background:`${T.panel}88`, backdropFilter:"blur(8px)",
                        height:"100%", minHeight:0, overflow:"hidden" }}>
            {panelChat}
          </div>
        </div>
      )}
    </div>
  );
}

function Esperando({ T, nombre }) {
  return (
    <div style={{ background:T.panel, border:`1px solid ${T.line}`, borderRadius:18,
                  padding:28, maxWidth:520 }}>
      <h2 style={{ fontFamily:"Georgia, serif", fontSize:24, margin:"0 0 10px" }}>
        Falta que entre la otra persona
      </h2>
      <p style={{ color:T.dim, fontSize:14, lineHeight:1.7 }}>
        Tu identidad quedó guardada como <b style={{ color:T.ink }}>{nombre}</b>.
        Hasta que ella entre con el mismo código y su propia frase, no hay clave de
        conversación: ni el chat ni los juegos pueden abrirse todavía.
      </p>
    </div>
  );
}

/* =========================================================
   Entrada — aquí se decide quién es quién
   ========================================================= */
function Entrada({ onEntrar }) {
  const N = TEMAS.normal;
  const [nombre, setNombre] = useState("");
  const [codigo, setCodigo] = useState("");
  const [frase, setFrase] = useState("");
  const [recordar, setRecordar] = useState(false);
  const [aviso, setAviso] = useState("");
  const [cargando, setCargando] = useState(false);

  useEffect(() => {
    try {
      const g = JSON.parse(localStorage.getItem("casita:recordado") || "{}");
      if (g.nombre) { setNombre(g.nombre); setRecordar(true); }
      if (g.codigo) setCodigo(g.codigo);
    } catch (e) {}
  }, []);

  const abrir = async () => {
    const nom = nombre.trim();
    if (!nom) return setAviso("Escribe tu nombre.");
    if (codigo.trim().length < 8) return setAviso("El código de sala necesita mínimo 8 caracteres.");
    if (frase.length < 6) return setAviso("Tu frase personal necesita mínimo 6 caracteres.");
    setCargando(true); setAviso("");

    let sala;
    try {
      sala = await derivarSala(codigo.trim().toLowerCase().replace(/\s+/g, "-"));
    } catch (e) {
      setCargando(false);
      return setAviso("Abre la página por https para que funcione el cifrado.");
    }

    try {
      const r = await fetch(`/api/sync?id=${sala.id}&v=-1&m=0&d=-1&espera=0`, { cache:"no-store" });
      const d = await r.json();

      let dir = { ranuras: {} };
      if (d.directorio) {
        try { dir = await descifrar(sala.sobre, d.directorio); }
        catch (e) {
          setCargando(false);
          return setAviso("Ese código no corresponde a esta sala.");
        }
      }

      let mias;
      if (dir.ranuras[nom]) {
        // ya existe: hay que probar que eres tú
        try { mias = await abrirIdentidad(dir.ranuras[nom], frase); }
        catch (e) {
          setCargando(false);
          return setAviso(`Esa frase no abre la identidad de ${nom}.`);
        }
      } else {
        if (Object.keys(dir.ranuras).length >= 2) {
          setCargando(false);
          return setAviso("Esta sala ya tiene sus dos personas.");
        }
        const nueva = await crearIdentidad(frase);
        dir = { ranuras: { ...dir.ranuras, [nom]: nueva.ranura } };
        mias = nueva.mias;
        await fetch(`/api/directorio?id=${sala.id}`, { method:"POST",
          headers:{ "Content-Type":"text/plain" }, body: await cifrar(sala.sobre, dir) });
      }

      try {
        if (recordar) localStorage.setItem("casita:recordado",
          JSON.stringify({ nombre: nom, codigo: codigo.trim().toLowerCase().replace(/\s+/g,"-") }));
        else localStorage.removeItem("casita:recordado");
      } catch (e) {}

      setCargando(false);
      onEntrar({ nombre: nom, id: sala.id, sobre: sala.sobre, mias }, dir);
    } catch (e) {
      setCargando(false);
      setAviso("No se pudo hablar con el servidor.");
    }
  };

  return (
    <div style={{ background:N.fondo, color:N.ink, minHeight:"100dvh", position:"relative", overflow:"hidden",
                  display:"flex", alignItems:"center", justifyContent:"center", padding:24 }}>
      <div className="aura" style={{ width:480, height:480, background:N.aura1, top:-120, left:-80 }} />
      <div style={{ background:N.panel, border:`1px solid ${N.line}`, maxWidth:440, width:"100%",
                    borderRadius:18, padding:30, position:"relative", boxShadow:"0 24px 70px rgba(0,0,0,.55)" }}>
        <h1 style={{ fontFamily:"Georgia, serif", color:N.gold, fontSize:44, margin:"0 0 8px",
                     textShadow:`0 0 26px ${N.glow}` }}>La casita</h1>
        <p style={{ color:N.dim, fontSize:14, lineHeight:1.6, marginBottom:24 }}>
          Solo caben dos. Cada uno con su propia frase, para que nadie pueda escribir por el otro.
        </p>

        <Campo label="Tu nombre" T={N} valor={nombre} set={setNombre} ph="Miguel" />
        <Campo label="Código de la sala (el mismo para los dos)" T={N} valor={codigo} set={setCodigo}
               ph="algo-que-solo-ustedes-sepan" />
        <Campo label="Tu frase personal (solo tuya)" T={N} valor={frase} set={setFrase}
               ph="no la compartas con nadie" tipo="password" onEnter={abrir} />

        <label style={{ display:"flex", gap:9, alignItems:"flex-start", color:N.dim, fontSize:13,
                        lineHeight:1.5, margin:"2px 0 18px", cursor:"pointer" }}>
          {/* colorScheme dark: sin esto el cuadrito sin marcar sale blanco contra el fondo oscuro */}
          <input type="checkbox" checked={recordar} onChange={e => setRecordar(e.target.checked)}
                 style={{ width:16, height:16, flexShrink:0, marginTop:2,
                          accentColor:N.hot, colorScheme:"dark" }} />
          Recordar mi nombre y el código en este dispositivo
        </label>

        <p style={{ color:N.dim, fontSize:12, lineHeight:1.6, marginBottom:20 }}>
          La frase nunca se guarda: se pide cada vez. Si la olvidas, pierdes tu identidad
          en esta sala y toca empezar de cero.
        </p>

        {aviso && <p style={{ color:N.hot, fontSize:14, marginBottom:14 }}>{aviso}</p>}
        <button onClick={abrir} disabled={cargando}
          style={{ width:"100%", background:N.hot, color:"#14060C", border:0, borderRadius:10,
                   padding:"14px 0", fontWeight:700, fontSize:15, cursor:"pointer",
                   boxShadow:`0 10px 30px ${N.aura1}` }}>
          {cargando ? "Abriendo…" : "Entrar"}
        </button>
      </div>
    </div>
  );
}

function Campo({ label, T, valor, set, ph, onEnter, tipo }) {
  return (
    <div style={{ marginBottom:16 }}>
      {label && <label style={{ color:T.dim, fontSize:12, display:"block", marginBottom:7 }}>{label}</label>}
      <input value={valor} onChange={e => set(e.target.value)} placeholder={ph} type={tipo || "text"}
             onKeyDown={e => e.key === "Enter" && onEnter && onEnter()}
             style={{ width:"100%", background:T.panel2, border:`1px solid ${T.line}`, color:T.ink,
                      borderRadius:10, padding:"13px 16px", fontSize:15 }} />
    </div>
  );
}

function Marcador({ estado, T }) {
  const e = Object.entries(estado.marcador || {});
  if (!e.length) return null;
  return (
    <div style={{ background:T.panel, border:`1px solid ${T.line}`, borderRadius:12,
                  padding:"10px 16px", marginBottom:20, display:"flex", flexWrap:"wrap", gap:24, fontSize:14 }}>
      {e.map(([n,p]) => (
        <span key={n} style={{ color:T.dim }}>{n} <b style={{ color:T.ok, fontSize:17 }}>{p}</b></span>
      ))}
    </div>
  );
}

/* =========================================================
   Chat
   ========================================================= */
/* Tablero de emojis. Va aqui, en texto plano, porque no hay CDN ni paquetes:
   son solo caracteres, no pesan nada y los pinta la fuente del sistema. */
const EMOJIS = {
  "Caritas": "😀 😃 😄 😁 😆 😅 🤣 😂 🙂 🙃 😉 😊 😇 🥰 😍 🤩 😘 😗 😚 😙 🥲 😋 😛 😜 🤪 😝 🤗 🤭 🤫 🤔 🤐 😐 😑 😶 😏 😒 🙄 😬 😮‍💨 😌 😔 😪 😴 🥱 😷 🤒 🤕 🥴 😵 🤯 🥳 😎 🤓 🧐 😕 😟 🙁 😮 😯 😲 😳 🥺 😦 😧 😨 😰 😥 😢 😭 😱 😖 😣 😞 😓 😩 😫 😤 😡 🤬",
  "Amor": "❤️ 🧡 💛 💚 💙 💜 🖤 🤍 🤎 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 😻 💋 👩‍❤️‍👨 💑 💏 🌹 🌺 🌷 🌸 💐 🥀 ✨ 💫 ⭐ 🌟 🔥 💯 🫶 🤟",
  "Gestos": "👋 🤚 ✋ 🖐️ 👌 🤌 🤏 ✌️ 🤞 🫰 🤙 👈 👉 👆 👇 ☝️ 👍 👎 ✊ 👊 🤛 🤜 👏 🙌 👐 🤲 🤝 🙏 💪 🫂 🫡 🤷 🤦 💁 🙋 🙆 🙅 🤗",
  "Animales": "🐶 🐱 🐭 🐹 🐰 🦊 🐻 🐼 🐨 🐯 🦁 🐮 🐷 🐸 🐵 🙈 🙉 🙊 🐔 🐧 🐦 🐤 🦆 🦉 🦇 🐺 🐗 🐴 🦄 🐝 🦋 🐌 🐞 🐢 🐍 🐙 🦀 🐠 🐬 🐳 🦈 🐊 🦥 🦦 🐘 🦒 🦘 🐕 🐈 🦜",
  "Comida": "🍏 🍎 🍐 🍊 🍋 🍌 🍉 🍇 🍓 🫐 🍒 🍑 🥭 🍍 🥥 🥝 🍅 🥑 🌽 🥕 🥔 🍞 🥐 🥖 🧀 🥚 🍳 🥞 🧇 🥓 🍔 🍟 🍕 🌭 🥪 🌮 🌯 🥗 🍝 🍜 🍲 🍣 🍤 🍦 🍩 🍪 🎂 🍰 🧁 🍫 🍬 🍭 🍿 ☕ 🍵 🧃 🍺 🍻 🥂 🍷 🧉",
  "Cosas": "🏠 🛏️ 🛋️ 🚿 🕯️ 💡 📱 💻 ⌨️ 🖥️ 🎧 🎤 🎸 🎹 🥁 🎮 🕹️ 🎲 🧩 🎯 ⚽ 🏀 🎾 🏐 🎳 🚗 🚕 🚌 🚲 ✈️ 🚀 🛸 ⛵ 🗺️ 🧳 📷 🎬 📚 ✏️ 📝 📌 📎 🔑 🔒 🎁 🎈 🎉 🎊 💰 ⏰ ⌛",
  "Lugares": "🌍 🌎 🌏 🌙 ☀️ 🌤️ ⛅ 🌧️ ⛈️ 🌨️ ❄️ ☃️ 🌈 🌊 🔥 💧 🏔️ ⛰️ 🌋 🏖️ 🏝️ 🏜️ 🌲 🌳 🌴 🌵 🍀 🌾 🌿 🍁 🍂 🌻 🌼 🌱 🪴 🏡 🏙️ 🌃 🌆 🌇 🎆 🎇",
  "Símbolos": "✅ ❌ ❓ ❗ ⚠️ 🔔 🔕 🔊 🔇 ♻️ 🆗 🆘 💤 💬 💭 🗯️ ➕ ➖ ✖️ ➗ 🔴 🟠 🟡 🟢 🔵 🟣 ⚫ ⚪ 🟥 🟧 🟨 🟩 🟦 🟪 ⬛ ⬜ ♥️ ♦️ ♣️ ♠️ 🎵 🎶 〽️ ⏳ ⏱️",
};

const MAX_RECIENTES = 24;
const leerRecientes = () => {
  try {
    const g = JSON.parse(localStorage.getItem("casita:emojis") || "[]");
    return Array.isArray(g) ? g.slice(0, MAX_RECIENTES) : [];
  } catch (e) { return []; }
};
const guardarRecientes = (lista) => {
  try { localStorage.setItem("casita:emojis", JSON.stringify(lista.slice(0, MAX_RECIENTES))); }
  catch (e) {}
};

/* Separadores de dia: los mensajes viven 48 h, asi que "Hoy" y "Ayer" alcanzan. */
const mismoDia = (a, b) => new Date(a).toDateString() === new Date(b).toDateString();
const diaDe = (t) => {
  const d = new Date(t), hoy = new Date();
  const ayer = new Date(hoy); ayer.setDate(hoy.getDate() - 1);
  if (mismoDia(d, hoy)) return "Hoy";
  if (mismoDia(d, ayer)) return "Ayer";
  return d.toLocaleDateString("es-CO", { weekday:"long", day:"numeric", month:"long" });
};

/* Un mensaje de puros emojis (hasta 3) se pinta grande y sin burbuja. */
const contarGrafemas = (s) => {
  try { return [...new Intl.Segmenter("es", { granularity:"grapheme" }).segment(s)].length; }
  catch (e) { return [...s].length; }
};
const soloEmojis = (s) => {
  const t = (s || "").trim();
  if (!t || /[0-9A-Za-zÀ-ÿ]/.test(t)) return 0;
  if (!/\p{Extended_Pictographic}/u.test(t)) return 0;
  if (!/^[\p{Extended_Pictographic}\p{Emoji_Component}‍️\s]+$/u.test(t)) return 0;
  const n = contarGrafemas(t.replace(/\s+/g, ""));
  return n <= 3 ? n : 0;
};

function TableroEmojis({ T, onElegir, onCerrar, recientes }) {
  const grupos = recientes.length ? { "Recientes": recientes.join(" "), ...EMOJIS } : EMOJIS;
  const claves = Object.keys(grupos);
  const [grupo, setGrupo] = useState(claves[0]);
  const lista = (grupos[grupo] || "").split(/\s+/).filter(Boolean);

  useEffect(() => {
    const f = (e) => { if (e.key === "Escape") onCerrar(); };
    window.addEventListener("keydown", f);
    return () => window.removeEventListener("keydown", f);
  }, [onCerrar]);

  return (
    <div style={{ borderTop:`1px solid ${T.line}`, background:T.panel2, display:"flex",
                  flexDirection:"column", maxHeight:250, flexShrink:0 }}>
      <div style={{ display:"flex", gap:6, overflowX:"auto", padding:"8px 10px",
                    borderBottom:`1px solid ${T.line}`, flexShrink:0 }}>
        {claves.map(k => (
          <button key={k} onClick={() => setGrupo(k)}
            style={{ background: grupo === k ? T.hot : "transparent",
                     color: grupo === k ? "#14060C" : T.dim,
                     border:`1px solid ${grupo === k ? T.hot : T.line}`, borderRadius:99,
                     padding:"5px 12px", fontSize:12, fontWeight:700, cursor:"pointer",
                     whiteSpace:"nowrap", flexShrink:0 }}>
            {k}
          </button>
        ))}
      </div>
      <div style={{ overflowY:"auto", padding:"8px 6px", display:"grid",
                    gridTemplateColumns:"repeat(auto-fill, minmax(44px, 1fr))", gap:2 }}>
        {lista.map((e, i) => (
          <button key={e + i} onClick={() => onElegir(e)} title={e}
            style={{ background:"none", border:0, fontSize:26, lineHeight:1.1, cursor:"pointer",
                     padding:"6px 0", borderRadius:10 }}>
            {e}
          </button>
        ))}
      </div>
    </div>
  );
}

const REACCIONES_RAPIDAS = ["❤️", "😂", "😮", "😢", "👍", "🔥"];

function Chat({ mensajes, yo, otro, T, enviar, conectado, angosto, listo,
                sonido, setSonido, salir, borrarChat,
                reacciones, reaccionar, escribiendo, avisarEscribiendo }) {
  const [texto, setTexto] = useState("");
  const [menu, setMenu] = useState(false);
  const [abierta, setAbierta] = useState(null);   // id del mensaje con la barra abierta
  const [tablero, setTablero] = useState(false);
  const [recientes, setRecientes] = useState(leerRecientes);
  const [aviso, setAviso] = useState("");
  const [mandando, setMandando] = useState(false);
  const fin = useRef(null);
  const campo = useRef(null);

  useEffect(() => { fin.current?.scrollIntoView({ behavior:"smooth" }); }, [mensajes.length]);

  const mandar = async () => {
    const t = texto.trim();
    if (!t || mandando) return;
    setMandando(true);
    const fue = await enviar(texto);
    setMandando(false);
    if (fue) { setTexto(""); setAviso(""); setTablero(false); }
    else setAviso("No salió. Revisa la conexión y vuelve a darle.");
  };

  // Inserta el emoji donde este el cursor, no siempre al final.
  const ponerEmoji = (e) => {
    const el = campo.current;
    const ini = el && typeof el.selectionStart === "number" ? el.selectionStart : texto.length;
    const fin2 = el && typeof el.selectionEnd === "number" ? el.selectionEnd : texto.length;
    const nuevo = (texto.slice(0, ini) + e + texto.slice(fin2)).slice(0, 800);
    setTexto(nuevo);
    setRecientes(prev => {
      const lista = [e, ...prev.filter(x => x !== e)].slice(0, MAX_RECIENTES);
      guardarRecientes(lista);
      return lista;
    });
    if (sonido) SONIDO.clic();
    requestAnimationFrame(() => {
      if (!el) return;
      const pos = ini + e.length;
      try { el.focus({ preventScroll:true }); el.setSelectionRange(pos, pos); } catch (err) {}
    });
  };

  return (
    <div style={{ display:"flex", flexDirection:"column", height:"100%", minHeight:0,
                  paddingBottom: angosto ? 68 : 0 }}>
      <div style={{ padding:"12px 14px", borderBottom:`1px solid ${T.line}`,
                    display:"flex", alignItems:"center", gap:9 }}>
        <span className={conectado ? "" : "respira"}
              style={{ width:9, height:9, borderRadius:99, background: conectado ? T.ok : T.hot,
                       boxShadow:`0 0 10px ${conectado ? T.ok : T.hot}`, flexShrink:0 }} />
        <span style={{ fontSize:13, color:T.dim, flex:1, overflow:"hidden", textOverflow:"ellipsis",
                       whiteSpace:"nowrap" }}>
          {!conectado ? "Reconectando…" : otro ? `Con ${otro} · cifrado de punta a punta` : "Esperando a la otra persona"}
        </span>
        <button onClick={() => setSonido(s => !s)}
                style={{ background:"none", border:0, color: sonido ? T.ok : T.dim, fontSize:16, cursor:"pointer" }}>
          {sonido ? "♪" : "✕"}
        </button>
        <button onClick={() => setMenu(m => !m)}
                style={{ background:"none", border:0, color:T.dim, fontSize:18, cursor:"pointer" }}>⋯</button>
      </div>

      {menu && (
        <div style={{ borderBottom:`1px solid ${T.line}`, padding:"10px 14px", display:"flex", gap:16 }}>
          <button onClick={borrarChat} style={{ background:"none", border:0, color:T.hot,
                                                fontSize:13, cursor:"pointer", padding:0 }}>
            Borrar el chat
          </button>
          <button onClick={salir} style={{ background:"none", border:0, color:T.dim,
                                           fontSize:13, cursor:"pointer", padding:0 }}>
            Salir de este dispositivo
          </button>
        </div>
      )}

      <div style={{ flex:1, minHeight:0, overflowY:"auto", padding:"14px 14px 6px", display:"flex",
                    flexDirection:"column", gap:9 }}>
        {!listo && (
          <p style={{ color:T.dim, fontSize:13, textAlign:"center", marginTop:30, lineHeight:1.7 }}>
            El chat se abre cuando entren los dos.<br />
            La clave se arma entre sus dos identidades.
          </p>
        )}
        {listo && mensajes.length === 0 && (
          <p style={{ color:T.dim, fontSize:13, textAlign:"center", marginTop:30, lineHeight:1.6 }}>
            Nada todavía.<br />Escribe algo.
          </p>
        )}
        {mensajes.map((m, i) => {
          const mio = m.autor === yo;
          const previo = mensajes[i - 1];
          const nuevoDia = !previo || !mismoDia(previo.t, m.t);
          const grandes = soloEmojis(m.texto);   // 0 = burbuja normal
          return (
            <React.Fragment key={i}>
              {nuevoDia && (
                <div style={{ alignSelf:"center", color:T.dim, fontSize:11, background:T.panel2,
                              border:`1px solid ${T.line}`, borderRadius:99, padding:"3px 12px",
                              margin:"6px 0 2px" }}>
                  {diaDe(m.t)}
                </div>
              )}
              <div className="burbuja" style={{ alignSelf: mio ? "flex-end" : "flex-start", maxWidth:"85%" }}>
                {!mio && <div style={{ fontSize:11, color:T.dim, marginBottom:3, paddingLeft:4 }}>{m.autor}</div>}
                {grandes ? (
                  <div style={{ fontSize: grandes === 1 ? 46 : grandes === 2 ? 38 : 32,
                                lineHeight:1.15, padding:"2px 4px",
                                textAlign: mio ? "right" : "left",
                                filter: m.ok === false ? "grayscale(1)" : "none" }}>
                    {m.texto}
                  </div>
                ) : (
                  <div style={{ background: mio ? T.hot : T.panel2,
                                color: mio ? "#14060C" : T.ink,
                                border: m.ok === false ? `1px solid ${T.hot}` : mio ? "none" : `1px solid ${T.line}`,
                                borderRadius: mio ? "14px 14px 4px 14px" : "14px 14px 14px 4px",
                                padding:"9px 13px", fontSize:14.5, lineHeight:1.45, wordBreak:"break-word" }}>
                    {m.texto}
                  </div>
                )}
                {(() => {
                  const puestas = (m.id && reacciones[m.id]) || {};
                  const mia = puestas[yo];
                  const lista = Object.entries(puestas);
                  return (
                    <>
                      <div style={{ display:"flex", alignItems:"center", gap:6, marginTop:3,
                                    flexDirection: mio ? "row-reverse" : "row" }}>
                        <span style={{ fontSize:10, color: m.ok === false ? T.hot : T.dim, padding:"0 4px" }}>
                          {m.ok === false ? "⚠ firma no verificada · " : ""}{hora(m.t)}
                        </span>
                        {m.id && (
                          <button onClick={() => setAbierta(a => a === m.id ? null : m.id)}
                                  title="Reaccionar" aria-label={"Reaccionar a " + m.autor}
                                  style={{ background:"none", border:0, padding:0, cursor:"pointer",
                                           fontSize:13, lineHeight:1, opacity: abierta === m.id ? 1 : .45,
                                           color:T.dim }}>
                            ☺+
                          </button>
                        )}
                        {lista.length > 0 && (
                          <span style={{ display:"flex", gap:3 }}>
                            {lista.map(([quien, em]) => (
                              <span key={quien} title={quien}
                                    style={{ background: quien === yo ? T.panel2 : "transparent",
                                             border:`1px solid ${quien === yo ? T.hot : T.line}`,
                                             borderRadius:99, padding:"1px 6px", fontSize:12,
                                             lineHeight:1.4 }}>
                                {em}
                              </span>
                            ))}
                          </span>
                        )}
                      </div>
                      {abierta === m.id && (
                        <div style={{ display:"flex", gap:2, marginTop:4, background:T.panel2,
                                      border:`1px solid ${T.line}`, borderRadius:99, padding:"3px 5px",
                                      alignSelf: mio ? "flex-end" : "flex-start", width:"fit-content" }}>
                          {REACCIONES_RAPIDAS.map(em => (
                            <button key={em}
                              onClick={() => { reaccionar(m.id, em, mia === em);
                                               setAbierta(null); if (sonido) SONIDO.clic(); }}
                              style={{ background: mia === em ? T.hot : "none", border:0,
                                       borderRadius:99, cursor:"pointer", fontSize:17,
                                       padding:"2px 5px", lineHeight:1 }}>
                              {em}
                            </button>
                          ))}
                        </div>
                      )}
                    </>
                  );
                })()}
              </div>
            </React.Fragment>
          );
        })}
        <div ref={fin} />
      </div>

      {listo && escribiendo && (
        <div style={{ padding:"4px 16px 0", display:"flex", alignItems:"center", gap:7,
                      color:T.dim, fontSize:12, flexShrink:0 }}>
          <span className="respira" style={{ width:6, height:6, borderRadius:99,
                                             background:T.ok, flexShrink:0 }} />
          {escribiendo} está escribiendo…
        </div>
      )}

      {listo && tablero && (
        <TableroEmojis T={T} recientes={recientes} onElegir={ponerEmoji}
                       onCerrar={() => setTablero(false)} />
      )}

      {(aviso || texto.length > 700) && (
        <div style={{ padding:"6px 14px 0", display:"flex", gap:10, alignItems:"center",
                      fontSize:11, flexShrink:0 }}>
          {aviso && <span style={{ color:T.hot, flex:1 }}>{aviso}</span>}
          {texto.length > 700 && (
            <span style={{ color: texto.length >= 800 ? T.hot : T.dim, marginLeft:"auto" }}>
              {texto.length}/800
            </span>
          )}
        </div>
      )}

      <div style={{ padding:12, borderTop:`1px solid ${T.line}`, display:"flex", gap:8,
                    alignItems:"center", flexShrink:0 }}>
        <button onClick={() => { setTablero(t => !t); if (sonido) SONIDO.clic(); }} disabled={!listo}
                title="Emojis" aria-label="Emojis"
                style={{ background: tablero ? T.hot : T.panel2,
                         border:`1px solid ${tablero ? T.hot : T.line}`, borderRadius:99,
                         width:44, height:44, fontSize:20, flexShrink:0, padding:0,
                         cursor: listo ? "pointer" : "default", opacity: listo ? 1 : .5 }}>
          {tablero ? "⌨" : "🙂"}
        </button>
        <input ref={campo} value={texto} disabled={!listo}
               onChange={e => { setTexto(e.target.value); if (aviso) setAviso("");
                                if (e.target.value.trim()) avisarEscribiendo(); }}
               onKeyDown={e => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); mandar(); } }}
               placeholder={listo ? "Escribe…" : "Esperando…"} maxLength={800}
               style={{ flex:1, minWidth:0, background:T.panel2, border:`1px solid ${T.line}`, color:T.ink,
                        borderRadius:99, padding:"11px 16px", fontSize:14.5, opacity: listo ? 1 : .5 }} />
        <button onClick={mandar} disabled={!listo || mandando || !texto.trim()}
                style={{ background:T.hot, color:"#14060C", border:0, borderRadius:99, width:44, height:44,
                         fontSize:17, cursor: listo ? "pointer" : "default", flexShrink:0,
                         opacity: (listo && !mandando && texto.trim()) ? 1 : .5 }}>
          {mandando ? "…" : "➤"}
        </button>
      </div>
    </div>
  );
}

/* =========================================================
   Juego 1 — Ahorcado
   ========================================================= */
const MAX_FALLOS = 6;
const LETRAS = "ABCDEFGHIJKLMNÑOPQRSTUVWXYZ".split("");
const RETOS = [
  "una prenda menos",
  "un audio diciendo qué extrañas",
  "una foto de lo que llevas puesto ahorita",
  "confesar una fantasía",
  "videollamada de 15 minutos, sin excusas",
  "quien pierde escoge película el viernes",
  "un mensaje de voz de 60 segundos, sin parar",
  "contar algo que nunca has dicho en voz alta",
];

const ahorcadoInicial = () => ({ palabra:"", pista:"", reto:"", autor:"", letras:[], fin:null });

function VistaAhorcado({ estado, publicar, yo, T, sonido }) {
  const d = estado.datos.ahorcado || ahorcadoInicial();
  const [palabraIn, setPalabraIn] = useState("");
  const [pistaIn, setPistaIn] = useState("");
  const [retoIn, setRetoIn] = useState("");
  const [aviso, setAviso] = useState("");
  const [sacude, setSacude] = useState(false);
  const [fiesta, setFiesta] = useState(null);
  const previo = useRef({ fallos:0, aciertos:0, terminada:false });

  const objetivo = norm(d.palabra);
  const fallos = d.letras.filter(l => objetivo && !objetivo.includes(l)).length;
  const aciertos = d.letras.filter(l => objetivo && objetivo.includes(l)).length;
  const adivinada = objetivo && objetivo.split("").every(ch => ch === " " || d.letras.includes(ch));
  const perdida = fallos >= MAX_FALLOS;
  const terminada = Boolean(objetivo) && (adivinada || perdida);
  const soyAutor = d.autor === yo;
  const hayRonda = Boolean(d.palabra);
  const vidas = MAX_FALLOS - fallos;

  const guardar = (nuevoD, extra = {}) =>
    publicar({ ...estado, ...extra, datos: { ...estado.datos, ahorcado: nuevoD } });

  useEffect(() => {
    const p = previo.current;
    if (fallos > p.fallos) {
      if (sonido) SONIDO.fallo();
      setSacude(true); setTimeout(() => setSacude(false), 500);
    } else if (aciertos > p.aciertos && sonido) SONIDO.acierto();
    if (terminada && !p.terminada) {
      setFiesta(adivinada ? "confeti" : "corazones");
      setTimeout(() => setFiesta(null), 3600);
      if (sonido) (adivinada ? SONIDO.gana : SONIDO.pierde)();
    }
    previo.current = { fallos, aciertos, terminada };
  }, [fallos, aciertos, terminada, adivinada, sonido]);

  useEffect(() => {
    if (!terminada || d.fin || soyAutor) return;
    const ganador = adivinada ? yo : d.autor;
    const marcador = { ...(estado.marcador || {}) };
    marcador[ganador] = (marcador[ganador] || 0) + 1;
    guardar({ ...d, fin: adivinada ? "ganó" : "perdió" }, { marcador });
  }, [terminada, adivinada, d, soyAutor, yo]);

  const jugarLetra = (l) => {
    if (terminada || d.letras.includes(l) || soyAutor || !hayRonda) return;
    guardar({ ...d, letras: [...d.letras, l] });
  };

  useEffect(() => {
    const onKey = (e) => {
      if (["INPUT","TEXTAREA"].includes(e.target.tagName)) return;
      const k = norm(e.key);
      if (k.length === 1 && LETRAS.includes(k)) jugarLetra(k);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  });

  const ponerPalabra = () => {
    const limpia = palabraIn.trim();
    if (limpia.length < 3) return setAviso("La palabra necesita mínimo 3 letras.");
    if (!/^[a-zA-ZáéíóúüñÁÉÍÓÚÜÑ ]+$/.test(limpia)) return setAviso("Solo letras y espacios.");
    guardar({ palabra:limpia, pista:pistaIn.trim(), reto:retoIn.trim(), autor:yo, letras:[], fin:null });
    setPalabraIn(""); setPistaIn(""); setRetoIn(""); setAviso("");
  };

  if (!hayRonda) {
    return (
      <div style={{ background:T.panel, border:`1px solid ${T.line}`, borderRadius:18, padding:26, maxWidth:520 }}>
        <h2 style={{ fontFamily:"Georgia, serif", fontSize:24, margin:"0 0 8px" }}>Pon una palabra</h2>
        <p style={{ color:T.dim, fontSize:14, marginBottom:20 }}>
          Quien escriba primero es quien reta. El otro verá los guiones al instante.
        </p>
        <Campo T={T} valor={palabraIn} set={setPalabraIn} ph="Palabra o frase corta" />
        <Campo T={T} valor={pistaIn} set={setPistaIn} ph="Pista (opcional)" />
        <Campo T={T} valor={retoIn} set={setRetoIn} ph="Qué se juega si pierde (opcional)" onEnter={ponerPalabra} />
        <button onClick={() => setRetoIn(RETOS[Math.floor(Math.random()*RETOS.length)])}
          style={{ background:"none", border:0, color:T.ok, fontSize:12, textDecoration:"underline",
                   cursor:"pointer", padding:0, margin:"2px 0 20px", display:"block" }}>
          Sugerir una apuesta
        </button>
        {aviso && <p style={{ color:T.hot, fontSize:14, marginBottom:14 }}>{aviso}</p>}
        <button onClick={ponerPalabra}
          style={{ background:T.hot, color:"#14060C", border:0, borderRadius:10, padding:"13px 24px",
                   fontWeight:700, fontSize:15, cursor:"pointer", boxShadow:`0 10px 30px ${T.aura1}` }}>
          Enviar palabra
        </button>
      </div>
    );
  }

  return (
    <div style={{ display:"flex", flexDirection:"column", gap:22 }}>
      {fiesta === "confeti" && <Confeti T={T} />}
      {fiesta === "corazones" && <Corazones T={T} />}

      <div style={{ display:"flex", flexWrap:"wrap", gap:28, alignItems:"flex-start" }}>
        <div className={sacude ? "sacudir" : ""}><Horca fallos={fallos} perdida={perdida} T={T} /></div>

        <div style={{ flex:1, minWidth:270 }}>
          <div style={{ display:"flex", gap:8, marginBottom:18 }}>
            {Array.from({ length:MAX_FALLOS }).map((_,i) => (
              <span key={i} className={i === vidas ? "latir" : ""}
                style={{ width:13, height:13, borderRadius:99, display:"inline-block",
                         background: i < vidas ? T.ok : "transparent",
                         border:`2px solid ${i < vidas ? T.ok : T.line}`,
                         boxShadow: i < vidas ? `0 0 12px ${T.glow}` : "none" }} />
            ))}
          </div>

          {d.pista && <p style={{ color:T.dim, fontSize:14, marginBottom:12 }}>
            Pista: <b style={{ color:T.ink }}>{d.pista}</b></p>}
          {d.reto && <p style={{ color:T.hot, fontSize:14, marginBottom:18, border:`1px solid ${T.line}`,
                                 background:T.panel, borderRadius:10, padding:"8px 12px", display:"inline-block" }}>
            En juego: <b style={{ color:T.ink }}>{d.reto}</b></p>}

          <div style={{ display:"flex", flexWrap:"wrap", gap:8, marginBottom:18, perspective:600 }}>
            {d.palabra.split("").map((ch,i) => {
              const n = norm(ch);
              if (ch === " ") return <span key={i} style={{ width:20 }} />;
              const acertada = d.letras.includes(n);
              const visible = acertada || terminada;
              return (
                <span key={i} className={acertada ? "voltear" : ""}
                  style={{ display:"inline-block", width:32, textAlign:"center", fontSize:30,
                           fontFamily:"Georgia, serif",
                           color: visible ? (acertada ? T.gold : T.hot) : "transparent",
                           borderBottom:`2px solid ${acertada ? T.gold : T.line}`, paddingBottom:4,
                           textShadow: acertada ? `0 0 18px ${T.glow}` : "none" }}>
                  {visible ? ch.toUpperCase() : "·"}
                </span>
              );
            })}
          </div>

          {fallos > 0 && !terminada && (
            <p style={{ color:T.dim, fontSize:14, marginBottom:12 }}>
              Falladas: <b style={{ color:T.hot, letterSpacing:4 }}>
                {d.letras.filter(l => !objetivo.includes(l)).join(" ")}</b>
            </p>
          )}

          {!terminada && (
            <p style={{ color:T.dim, fontSize:14 }}>
              {soyAutor ? "Tu palabra está en juego. Aguanta la respiración."
                : vidas === 1 ? "Último intento. Todo o nada."
                : `Te quedan ${vidas} intentos.`}
            </p>
          )}

          {terminada && (
            <div style={{ background:T.panel, border:`1px solid ${T.line}`, borderRadius:12, padding:18 }}>
              <p style={{ color: adivinada ? T.ok : T.hot, fontFamily:"Georgia, serif", fontSize:27,
                          margin:"0 0 8px", textShadow:`0 0 22px ${T.glow}` }}>
                {adivinada ? "¡Adivinada!" : "Se acabaron los intentos"}
              </p>
              <p style={{ color:T.dim, fontSize:14, marginBottom:10 }}>
                La palabra era <b style={{ color:T.ink }}>{d.palabra.toUpperCase()}</b>
              </p>
              {d.reto && (
                <p style={{ color:T.ink, fontSize:14, marginBottom:14 }}>
                  {adivinada ? `${d.autor} paga: ` : "Toca pagar: "}
                  <b style={{ color:T.hot }}>{d.reto}</b>
                </p>
              )}
              <button onClick={() => guardar(ahorcadoInicial())}
                style={{ background:T.hot, color:"#14060C", border:0, borderRadius:10, padding:"10px 20px",
                         fontWeight:700, cursor:"pointer" }}>Otra ronda</button>
            </div>
          )}
        </div>
      </div>

      {!terminada && (
        <div style={{ display:"flex", flexWrap:"wrap", gap:7 }}>
          {LETRAS.map(l => {
            const usada = d.letras.includes(l);
            const buena = usada && objetivo.includes(l);
            return (
              <button key={l} onClick={() => jugarLetra(l)} disabled={usada || soyAutor} className="tecla"
                style={{ width:44, height:44, fontSize:16, fontWeight:700, borderRadius:10,
                         cursor: usada || soyAutor ? "default" : "pointer",
                         background: usada ? (buena ? T.ok : T.panel2) : T.panel,
                         color: usada ? (buena ? "#160A10" : T.dim) : T.ink,
                         border:`1px solid ${usada ? "transparent" : T.line}`,
                         opacity: soyAutor && !usada ? .3 : usada && !buena ? .4 : 1,
                         boxShadow: buena ? `0 0 18px ${T.glow}` : "none",
                         transform: usada ? "scale(.9)" : "none" }}>{l}</button>
            );
          })}
        </div>
      )}
    </div>
  );
}

function Horca({ fallos, perdida, T }) {
  const partes = [
    <circle key="c" cx="150" cy="70" r="25" />,
    <line key="t" x1="150" y1="95" x2="150" y2="168" />,
    <line key="bi" x1="150" y1="114" x2="110" y2="144" />,
    <line key="bd" x1="150" y1="114" x2="190" y2="144" />,
    <line key="pi" x1="150" y1="168" x2="116" y2="216" />,
    <line key="pd" x1="150" y1="168" x2="184" y2="216" />,
  ];
  return (
    <svg width="230" height="270" viewBox="0 0 240 280">
      <ellipse cx="80" cy="256" rx="62" ry="7" fill="rgba(0,0,0,.45)" />
      <g stroke={T.line} strokeWidth="7" strokeLinecap="round">
        <line x1="20" y1="252" x2="140" y2="252" />
        <line x1="60" y1="252" x2="60" y2="16" />
        <line x1="60" y1="16" x2="150" y2="16" />
      </g>
      <path d="M150 16 q6 16 0 30" stroke={T.line} strokeWidth="4" fill="none" strokeLinecap="round" />
      <g className={perdida ? "colgado" : ""}>
        <g stroke={T.hot} strokeWidth="6" strokeLinecap="round" fill="none"
           style={{ filter:`drop-shadow(0 0 8px ${T.glow})` }}>
          {partes.slice(0, fallos).map((p,i) => <g key={i} className="parte">{p}</g>)}
        </g>
        {fallos >= 1 && <Cara fallos={fallos} T={T} />}
      </g>
    </svg>
  );
}

function Cara({ fallos, T }) {
  if (fallos >= 6) return (
    <g stroke={T.hot} strokeWidth="3.5" strokeLinecap="round" fill="none">
      <line x1="136" y1="60" x2="145" y2="69" /><line x1="145" y1="60" x2="136" y2="69" />
      <line x1="155" y1="60" x2="164" y2="69" /><line x1="164" y1="60" x2="155" y2="69" />
      <path d="M139 83 q11 -9 22 0" />
    </g>
  );
  return (
    <g>
      <circle cx="141" cy="64" r="3.4" fill={T.hot} />
      <circle cx="159" cy="64" r="3.4" fill={T.hot} />
      <path d={fallos >= 4 ? "M139 83 q11 -10 22 0" : fallos >= 2 ? "M140 80 h20" : "M140 76 q10 8 20 0"}
            stroke={T.hot} strokeWidth="3.5" strokeLinecap="round" fill="none" />
    </g>
  );
}

/* =========================================================
   Juego 2 — Tres en raya
   ========================================================= */
const gatoInicial = () => ({ tablero: Array(9).fill(null), turno:"X", jugadores:{}, fin:null });
const LINEAS = [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]];

function ganadorGato(t) {
  for (const [a,b,c] of LINEAS)
    if (t[a] && t[a] === t[b] && t[a] === t[c]) return { simbolo:t[a], linea:[a,b,c] };
  return t.every(Boolean) ? { simbolo:"empate", linea:[] } : null;
}

function VistaGato({ estado, publicar, yo, T, sonido }) {
  const d = estado.datos.gato || gatoInicial();
  const g = ganadorGato(d.tablero);
  const mio = Object.entries(d.jugadores).find(([, n]) => n === yo)?.[0] || null;
  const miTurno = mio ? d.turno === mio : Object.keys(d.jugadores).length < 2;
  const previo = useRef(null);

  const guardar = (nuevoD, extra = {}) =>
    publicar({ ...estado, ...extra, datos: { ...estado.datos, gato: nuevoD } });

  useEffect(() => {
    if (g && previo.current !== g.simbolo) {
      if (sonido) (g.simbolo === "empate" ? SONIDO.clic : (g.simbolo === mio ? SONIDO.gana : SONIDO.pierde))();
      if (g.simbolo !== "empate" && !d.fin) {
        const ganador = d.jugadores[g.simbolo];
        const marcador = { ...(estado.marcador || {}) };
        if (ganador) marcador[ganador] = (marcador[ganador] || 0) + 1;
        guardar({ ...d, fin: g.simbolo }, { marcador });
      }
    }
    previo.current = g?.simbolo || null;
  }, [g, mio]);

  const jugar = (i) => {
    if (g || d.tablero[i]) return;
    let jugadores = { ...d.jugadores }, simbolo = mio;
    if (!simbolo) {
      simbolo = jugadores.X ? "O" : "X";
      if (jugadores[simbolo]) return;
      jugadores[simbolo] = yo;
    }
    if (d.turno !== simbolo) return;
    const tablero = [...d.tablero];
    tablero[i] = simbolo;
    if (sonido) SONIDO.clic();
    guardar({ ...d, tablero, jugadores, turno: simbolo === "X" ? "O" : "X" });
  };

  return (
    <div style={{ display:"flex", flexWrap:"wrap", gap:28, alignItems:"flex-start" }}>
      <div style={{ display:"grid", gridTemplateColumns:"repeat(3, 92px)", gap:8 }}>
        {d.tablero.map((v,i) => {
          const gana = g?.linea.includes(i);
          return (
            <button key={i} onClick={() => jugar(i)} disabled={Boolean(v || g)} className="casilla"
              style={{ width:92, height:92, fontSize:44, fontFamily:"Georgia, serif", fontWeight:700,
                       borderRadius:14, cursor: v || g ? "default" : "pointer",
                       background: gana ? T.ok : T.panel,
                       color: gana ? "#14060C" : v === "X" ? T.gold : T.hot,
                       border:`1px solid ${T.line}`,
                       boxShadow: gana ? `0 0 22px ${T.glow}` : "none" }}>
              {v || ""}
            </button>
          );
        })}
      </div>

      <div style={{ flex:1, minWidth:220 }}>
        <p style={{ color:T.dim, fontSize:14, marginBottom:12 }}>
          {mio ? <>Juegas con <b style={{ color: mio === "X" ? T.gold : T.hot, fontSize:18 }}>{mio}</b></>
               : "Toca una casilla para tomar tu símbolo."}
        </p>
        {!g && mio && (
          <p style={{ color: miTurno ? T.ok : T.dim, fontSize:15, marginBottom:16 }}>
            {miTurno ? "Es tu turno." : "Esperando a que juegue."}
          </p>
        )}
        {g && (
          <div style={{ background:T.panel, border:`1px solid ${T.line}`, borderRadius:12, padding:18 }}>
            <p style={{ fontFamily:"Georgia, serif", fontSize:25, margin:"0 0 12px",
                        color: g.simbolo === "empate" ? T.dim : T.ok, textShadow:`0 0 20px ${T.glow}` }}>
              {g.simbolo === "empate" ? "Empate" : `Ganó ${d.jugadores[g.simbolo] || g.simbolo}`}
            </p>
            <button onClick={() => guardar({ ...gatoInicial(), jugadores: d.jugadores })}
              style={{ background:T.hot, color:"#14060C", border:0, borderRadius:10, padding:"10px 20px",
                       fontWeight:700, cursor:"pointer" }}>Otra vez</button>
          </div>
        )}
      </div>
    </div>
  );
}

/* =========================================================
   Efectos
   ========================================================= */
function Confeti({ T }) {
  const colores = [T.gold, T.ok, T.hot, T.ink];
  const trozos = useMemo(() => Array.from({ length:55 }, (_,i) => ({
    left: Math.random()*100, delay: Math.random()*.9, dur: 1.9 + Math.random()*1.5,
    color: colores[i % colores.length], ancho: 6 + Math.random()*8, alto: 10 + Math.random()*10,
  })), []);
  return trozos.map((t,i) => (
    <span key={i} className="papelito"
      style={{ left:`${t.left}%`, background:t.color, width:t.ancho, height:t.alto, borderRadius:2,
               animationDelay:`${t.delay}s`, animationDuration:`${t.dur}s` }} />
  ));
}

function Corazones({ T }) {
  const trozos = useMemo(() => Array.from({ length:22 }, () => ({
    left: 5 + Math.random()*90, delay: Math.random()*1.2, dur: 2.6 + Math.random()*1.6, tam: 16 + Math.random()*20,
  })), []);
  return trozos.map((t,i) => (
    <span key={i} className="corazon"
      style={{ left:`${t.left}%`, fontSize:t.tam, color:T.hot, animationDelay:`${t.delay}s`,
               animationDuration:`${t.dur}s`, textShadow:`0 0 16px ${T.glow}` }}>♥</span>
  ));
}

/* =========================================================
   Registro de juegos — añade el tuyo aquí
   ========================================================= */
const JUEGOS = {
  ahorcado: { nombre: "Ahorcado", inicial: ahorcadoInicial, Vista: VistaAhorcado },
  gato:     { nombre: "Tres en raya", inicial: gatoInicial, Vista: VistaGato },
};

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