// SmartInput — input siempre visible flotante con voz, sugerencias y categoría inferida (Bubblegum)

function SmartInput({ app, theme, placement = 'floating' }) {
  const c = theme.c;
  const t = app.t;
  const inline = placement === 'inline';
  const [name, setName]               = React.useState('');
  const [qty, setQty]                 = React.useState('');
  const [showQty, setShowQty]         = React.useState(false);
  const [listening, setListening]     = React.useState(false);
  const [focused, setFocused]         = React.useState(false);
  const [suggestions, setSuggestions] = React.useState([]);
  const [justAdded, setJustAdded]     = React.useState(null);
  const inputRef = React.useRef(null);

  const [manualCat, setManualCat] = React.useState(null);
  const [catSheetOpen, setCatSheetOpen] = React.useState(false);
  const [micPermDenied, setMicPermDenied] = React.useState(false);
  const [bulk, setBulk] = React.useState(null); // { lines, raw }
  const recRef = React.useRef(null);

  // Pegar texto con varias entradas (saltos de línea o comas) → confirmar e
  // insertar cada una como producto. Si solo hay 1 entrada dejamos el paste
  // por defecto para no interrumpir el flujo normal de tecleo.
  function detectBulkFromText(txt) {
    if (!txt) return null;
    const hasNewline = /[\n\r]/.test(txt);
    const hasComma   = txt.includes(',');
    if (!hasNewline && !hasComma) return null;
    // Split por newlines y comas a la vez.
    const lines = txt.split(/[\r\n,]+/).map(s => s.trim()).filter(Boolean);
    return lines.length >= 2 ? lines : null;
  }

  async function handlePaste(e) {
    // En WKWebView (Capacitor iOS) `clipboardData` puede llegar vacío. Tomamos
    // siempre el control: preventDefault, leer texto, decidir nosotros.
    e.preventDefault();
    let txt = e.clipboardData?.getData('text') || '';
    if (!txt) {
      try {
        const CB = window.Capacitor?.Plugins?.Clipboard;
        if (CB?.read) { const r = await CB.read(); txt = r?.value || ''; }
      } catch (_) {}
    }
    if (!txt && navigator.clipboard?.readText) {
      try { txt = await navigator.clipboard.readText(); } catch (_) {}
    }
    if (!txt) return;
    const lines = detectBulkFromText(txt);
    if (lines) { setBulk({ lines, raw: txt }); return; }
    setName(prev => (prev || '') + txt);
  }

  // iOS WKWebView a veces inserta el texto pegado vía evento `input` en lugar
  // de disparar `paste`, y `<input>` puede convertir `\n` antes de que
  // `onChange` lo vea. Cubrimos ambos casos: si por algún motivo llega un
  // valor multilínea aquí, abrimos el sheet de bulk-add y vaciamos el input.
  function handleChange(e) {
    const v = e.target.value;
    const lines = detectBulkFromText(v);
    if (lines) {
      setBulk({ lines, raw: v });
      setName('');
      return;
    }
    setName(v);
  }

  function confirmBulk() {
    if (!bulk) return;
    bulk.lines.forEach(n => app.addItem(n));
    if (navigator.vibrate) navigator.vibrate(15);
    setJustAdded(bulk.lines.length + '×');
    setTimeout(() => setJustAdded(null), 1400);
    setBulk(null);
  }

  function keepBulkAsOne() {
    if (!bulk) return;
    // Colapsa el texto pegado a una sola línea conservando comas/espacios.
    const collapsed = bulk.raw.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim();
    setName(prev => (prev || '') + collapsed);
    setBulk(null);
    requestAnimationFrame(() => inputRef.current?.focus());
  }

  // Lanza Ajustes nativos. Igual que en JoinSheet (QR camera deny).
  function openAppSettings() {
    try {
      const NS = window.Capacitor?.Plugins?.NativeSettings;
      if (NS?.open) { NS.open({ optionIOS: 'app', optionAndroid: 'application_details' }); return; }
    } catch (_) {}
    try { window.open('app-settings:', '_system'); } catch (_) {}
  }

  // Si el usuario pulsa el mic mientras está escuchando, paramos en vez de
  // ignorar el click. El SRPlugin no tiene stop() en todas las versiones, así
  // que llamamos al método si existe; si no, dejamos que termine solo.
  async function stopListening() {
    const ref = recRef.current;
    if (!ref) return;
    try {
      if (typeof ref.stop === 'function') {
        const r = ref.stop();
        if (r?.then) await r;
      }
    } catch (_) {}
    recRef.current = null;
    setListening(false);
  }

  // Empuja el input flotante por encima del teclado virtual en móvil. Solo
  // aplica al modo floating: en inline el scroll natural se encarga.
  const kbOffset = useKeyboardOffset(!inline);

  // Categoría efectiva: override manual > auto > 'otros'.
  const detectedCat = React.useMemo(() => {
    if (manualCat) return manualCat;
    if (!name.trim()) return null;
    if (app.autoCategory) return autoCategoryFor(name);
    return 'otros';
  }, [name, app.autoCategory, manualCat]);

  // Reset override cuando se vacía el input (item ya añadido o cancelado).
  React.useEffect(() => { if (!name.trim()) setManualCat(null); }, [name]);

  React.useEffect(() => {
    const q = name.toLowerCase().trim();
    if (!q) { setSuggestions([]); return; }
    const source = app.frequents || FREQUENT[app.lang];
    const freq = source.filter(f => f.toLowerCase().startsWith(q) && f.toLowerCase() !== q).slice(0, 3);
    setSuggestions(freq);
  }, [name, app.lang, app.frequents]);

  function submit(forced, { keepFocus = true } = {}) {
    const n = (forced || name).trim();
    if (!n) return;
    // Si el usuario ha forzado una categoría manualmente, la pasamos a
    // addItem para saltarse el auto-detect.
    const item = app.addItem(n, qty, manualCat || undefined);
    setName(''); setQty(''); setShowQty(false);
    setManualCat(null);
    setJustAdded(item ? item.name : n);
    setTimeout(() => setJustAdded(null), 1400);
    if (navigator.vibrate) navigator.vibrate(10);
    // Re-focus para que el teclado no se oculte entre items mientras
    // tecleas. Saltamos esto cuando el alta viene de tocar un chip de
    // ajuste rápido: el input nunca estuvo enfocado y no queremos abrir
    // el teclado en móvil tras un simple tap.
    if (keepFocus) requestAnimationFrame(() => inputRef.current?.focus());
  }

  // Voz. En Capacitor (iOS/Android) la Web Speech API no existe; usamos el
  // plugin nativo @capacitor-community/speech-recognition que pide permiso de
  // micrófono + reconocimiento, dicta una frase y devuelve transcript. En web
  // (Chrome, etc.) seguimos usando Web Speech.
  async function startListening(e) {
    if (e && e.preventDefault) e.preventDefault();
    // Toggle: si ya está escuchando, segundo click = parar.
    if (recRef.current || listening) { stopListening(); return; }
    setMicPermDenied(false);
    setListening(true);
    setFocused(true);

    const SRPlugin = (typeof window !== 'undefined') ? window.Capacitor?.Plugins?.SpeechRecognition : null;
    if (SRPlugin?.start) {
      try {
        const avail = await SRPlugin.available();
        if (!avail?.available) throw new Error('speech-not-available');
        const perm = await SRPlugin.checkPermissions();
        const perms = [perm?.speechRecognition, perm?.microphone];
        if (perms.some(p => p !== 'granted')) {
          const req = await SRPlugin.requestPermissions();
          if ([req?.speechRecognition, req?.microphone].some(p => p !== 'granted')) {
            setMicPermDenied(true);
            setListening(false);
            return;
          }
        }
        // Marca recRef antes de start() para que stopListening pueda cortar.
        recRef.current = SRPlugin;
        const res = await SRPlugin.start({
          language: app.lang === 'es' ? 'es-ES' : 'en-US',
          maxResults: 1,
          partialResults: false,
          popup: false,
        });
        const transcript = Array.isArray(res?.matches) ? res.matches[0] : res?.value?.[0];
        if (transcript) setName(transcript);
      } catch (err) {
        // iOS / Android lanzan errores con .message tipo "User denied access"
        const msg = String(err?.message || err || '').toLowerCase();
        if (msg.includes('denied') || msg.includes('not authorized') || msg.includes('permission')) {
          setMicPermDenied(true);
        }
      }
      recRef.current = null;
      setListening(false);
      return;
    }

    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (SR) {
      const rec = new SR();
      recRef.current = rec;
      rec.lang = app.lang === 'es' ? 'es-ES' : 'en-US';
      rec.interimResults = false;
      rec.maxAlternatives = 1;
      const cleanup = () => { recRef.current = null; setListening(false); };
      rec.onresult = (ev) => {
        const transcript = ev.results[0][0].transcript;
        setName(transcript);
        cleanup();
      };
      rec.onerror = (ev) => {
        if (ev?.error === 'not-allowed' || ev?.error === 'service-not-allowed') {
          setMicPermDenied(true);
        }
        cleanup();
      };
      rec.onend = cleanup;
      try { rec.start(); } catch (_) { cleanup(); }
      return;
    }

    // Fallback simulado
    setTimeout(() => {
      const samples = app.lang === 'es' ? ['Aguacates', 'Leche de avena', 'Pan de molde', 'Tomates'] : ['Avocados', 'Oat milk', 'Sandwich bread', 'Tomatoes'];
      setName(samples[Math.floor(Math.random() * samples.length)]);
      setListening(false);
    }, 1600);
  }

  const cat = detectedCat ? CATEGORIES[detectedCat] : null;

  const wrapperStyle = inline
    ? { position: 'sticky', top: 0, zIndex: 5, paddingBottom: 8, background: c.bg }
    : {
        position: 'absolute', left: 12, right: 12,
        bottom: kbOffset > 0
          ? kbOffset + 12
          : `calc(76px + env(safe-area-inset-bottom, 0px))`,
        zIndex: 20,
        transition: 'bottom .15s ease-out',
      };

  const chipsRow = (children) => (
    <div className="sl-scroll" style={{
      display: 'flex', gap: 6, overflow: 'auto',
      padding: inline ? '10px 0 0' : '4px 8px 10px',
      marginBottom: inline ? 0 : -4,
    }}>{children}</div>
  );

  const suggChips = focused && suggestions.length > 0 && !listening ? chipsRow(
    suggestions.map(s => (
      <button key={s} onClick={() => submit(s)}
        onMouseDown={e => e.preventDefault()}
        style={{
          padding: '8px 14px', borderRadius: 999,
          background: c.bgCard, color: c.ink,
          border: `1px solid ${c.border}`,
          fontSize: 13, fontWeight: 700, fontFamily: theme.fontDisplay,
          cursor: 'pointer', whiteSpace: 'nowrap',
          boxShadow: c.shadowSoft,
        }}>
        <span style={{ color: c.inkMuted, marginRight: 4 }}>+</span>{s}
      </button>
    ))
  ) : null;

  const freqChips = !focused && !name ? chipsRow(
    (app.frequents || FREQUENT[app.lang]).slice(0, 6).map(s => (
      <button key={s} onMouseDown={e => e.preventDefault()} onClick={() => submit(s, { keepFocus: false })}
        style={{
          padding: '7px 12px', borderRadius: 999,
          background: c.bgCard + 'd0',
          backdropFilter: 'blur(8px)', WebkitBackdropFilter: 'blur(8px)',
          color: c.inkSub,
          border: `1px solid ${c.border}`,
          fontSize: 12, fontWeight: 600, fontFamily: theme.fontBody,
          cursor: 'pointer', whiteSpace: 'nowrap',
        }}>
        + {s}
      </button>
    ))
  ) : null;

  return (
    <div style={wrapperStyle}>
      {!inline && suggChips}
      {!inline && freqChips}

      {justAdded && (
        // Wrapper externo centra (transform: translateX(-50%)). Wrapper interno
        // hace la animación de escala: si pusiéramos ambos en el mismo nodo,
        // los keyframes `transform: scale(...)` borrarían el translateX y el
        // toast aparecería pegado al borde derecho hasta que la animación
        // terminase — exactamente el trompicón que se ve en iPhone.
        <div style={{
          position: 'absolute', left: '50%', bottom: '100%', transform: 'translateX(-50%)',
          marginBottom: 14, pointerEvents: 'none',
        }}>
          <div style={{
            padding: '8px 14px',
            background: c.success, color: '#fff', borderRadius: 999,
            fontSize: 13, fontWeight: 700, fontFamily: theme.fontDisplay,
            whiteSpace: 'nowrap',
            animation: 'sl-bounce-in .3s cubic-bezier(.2,1.4,.4,1)',
            boxShadow: '0 6px 20px rgba(0,0,0,0.18)',
            transformOrigin: 'center bottom',
          }}>
            ✓ {justAdded}
          </div>
        </div>
      )}

      {micPermDenied && (
        <div className="sl-anim-slide" style={{
          display: 'flex', alignItems: 'center', gap: 10,
          padding: '10px 12px', marginBottom: 8,
          background: c.bgCard, color: c.ink,
          border: `1px solid ${c.border}`, borderRadius: theme.radius.lg,
          boxShadow: '0 4px 14px rgba(0,0,0,0.10)',
          fontFamily: theme.fontBody, fontSize: 13, lineHeight: 1.35,
        }}>
          <Icon name="mic" size={18} stroke={2.2} style={{ flexShrink: 0, color: c.danger }} />
          <span style={{ flex: 1, minWidth: 0 }}>
            {app.lang === 'es'
              ? 'Permiso de micrófono denegado. Actívalo en Ajustes para dictar.'
              : 'Microphone permission denied. Enable it in Settings to dictate.'}
          </span>
          <button onClick={openAppSettings} style={{
            padding: '6px 12px', borderRadius: theme.radius.pill,
            background: c.accent, color: c.accentInk, border: 'none',
            fontSize: 12, fontWeight: 800, fontFamily: theme.fontDisplay,
            cursor: 'pointer', flexShrink: 0, whiteSpace: 'nowrap',
          }}>{app.lang === 'es' ? 'Ajustes' : 'Settings'}</button>
          <button onClick={() => setMicPermDenied(false)} aria-label="dismiss" style={{
            width: 24, height: 24, borderRadius: '50%',
            background: 'transparent', color: c.inkMuted, border: 'none',
            cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
            flexShrink: 0,
          }}><Icon name="x" size={14} stroke={2.4} /></button>
        </div>
      )}

      <div style={{
        background: c.bgCard,
        borderRadius: theme.radius.xl,
        border: `1.5px solid ${focused ? c.accent : c.border}`,
        boxShadow: focused ? `0 8px 24px -8px ${c.accent}80` : '0 4px 14px rgba(0,0,0,0.08)',
        padding: showQty ? '10px 8px 10px 16px' : '6px 6px 6px 16px',
        transition: 'all .2s',
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          {cat ? (
            <button type="button"
              onMouseDown={(e) => e.preventDefault()}
              onClick={() => {
                inputRef.current?.blur();
                document.activeElement?.blur?.();
                // Espera a que el teclado se cierre antes de abrir el sheet:
                // si abrimos a la vez, el sheet emerge con el viewport aún
                // recortado y se ven menos categorías.
                const delay = app.isDesktop ? 0 : 220;
                setTimeout(() => setCatSheetOpen(true), delay);
              }}
              aria-label={app.lang === 'es' ? 'Cambiar categoría' : 'Change category'}
              title={app.lang === 'es' ? 'Cambiar categoría' : 'Change category'}
              style={{
                width: 32, height: 32, borderRadius: theme.radius.md,
                background: c.accent, color: c.accentInk,
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                fontSize: 17, flexShrink: 0,
                animation: 'sl-bounce-in .3s',
                border: 'none', cursor: 'pointer', padding: 0,
              }}><CatGlyph cat={cat} size={18} stroke={2.4} /></button>
          ) : (
            // Sin texto aún: si auto-cat está OFF, expone botón "+" que abre
            // el picker para fijar la categoría desde cero.
            <button type="button"
              onMouseDown={(e) => e.preventDefault()}
              onClick={() => {
                if (app.autoCategory) return;
                inputRef.current?.blur();
                document.activeElement?.blur?.();
                const delay = app.isDesktop ? 0 : 220;
                setTimeout(() => setCatSheetOpen(true), delay);
              }}
              aria-label={app.lang === 'es' ? 'Elegir categoría' : 'Pick category'}
              title={app.lang === 'es' ? 'Elegir categoría' : 'Pick category'}
              style={{
                background: 'transparent', border: 'none', padding: 0,
                cursor: app.autoCategory ? 'default' : 'pointer',
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                width: 32, height: 32, flexShrink: 0,
              }}>
              <Icon name="plus" size={22} stroke={2.6} style={{ color: c.inkMuted }} />
            </button>
          )}

          <input
            ref={inputRef}
            value={name}
            onChange={handleChange}
            onFocus={() => setFocused(true)}
            onBlur={() => setTimeout(() => setFocused(false), 150)}
            onKeyDown={e => { if (e.key === 'Enter') submit(); }}
            onPaste={handlePaste}
            placeholder={listening ? t.voiceListening : t.addPlaceholder}
            style={{
              flex: 1, minWidth: 0,
              border: 'none', outline: 'none', background: 'transparent',
              fontSize: 16, fontWeight: 600, fontFamily: theme.fontDisplay,
              color: c.ink, padding: '10px 0',
            }}
          />

          {name && !showQty && (
            <button onClick={() => setShowQty(true)} style={{
              padding: '6px 10px', borderRadius: 999,
              background: 'transparent', border: `1px solid ${c.border}`,
              color: c.inkSub, fontSize: 11, fontFamily: theme.fontMono,
              cursor: 'pointer', fontWeight: 600,
            }}>+ {t.qty}</button>
          )}

          <button
            onPointerDown={startListening}
            style={{
              width: 40, height: 40, borderRadius: '50%',
              border: 'none', flexShrink: 0,
              background: listening ? c.danger : 'transparent',
              color: listening ? '#fff' : c.inkSub,
              cursor: 'pointer',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              animation: listening ? 'sl-pulse-ring 1.4s ease-out infinite' : 'none',
              transition: 'all .15s',
            }}
          >
            {listening ? (
              <div style={{ display: 'flex', gap: 2, alignItems: 'flex-end', height: 16 }}>
                {[0,1,2,3].map(i => (
                  <span key={i} style={{
                    display: 'block', width: 2.5, height: 14, background: '#fff',
                    borderRadius: 2, transformOrigin: 'center',
                    animation: `sl-listening 0.7s ease-in-out infinite`,
                    animationDelay: `${i * 0.12}s`,
                  }} />
                ))}
              </div>
            ) : <Icon name="mic" size={18} stroke={2.4} />}
          </button>

          {name.trim() && !listening && (
            <button onClick={() => submit()} style={{
              width: 44, height: 44, borderRadius: '50%',
              border: 'none',
              background: c.accent, color: c.accentInk, cursor: 'pointer',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              flexShrink: 0, animation: 'sl-bounce-in .25s',
            }}>
              <Icon name="arrow-right" size={20} stroke={2.8} />
            </button>
          )}
        </div>

        {showQty && (
          <div style={{
            paddingTop: 10, marginTop: 6, borderTop: `1px solid ${c.border}`,
            display: 'flex', gap: 8, alignItems: 'center',
            animation: 'sl-slide-up .18s',
          }}>
            <span style={{ fontSize: 11, fontFamily: theme.fontMono, color: c.inkMuted, letterSpacing: 1, textTransform: 'uppercase', paddingRight: 4 }}>QTY</span>
            <input
              value={qty}
              onChange={e => setQty(e.target.value.replace(/\D/g, ''))}
              onKeyDown={e => { if (e.key === 'Enter') submit(); }}
              inputMode="numeric"
              pattern="[0-9]*"
              placeholder="1, 2, 5…"
              style={{
                flex: 1, border: 'none', outline: 'none', background: 'transparent',
                fontSize: 14, fontFamily: theme.fontMono, color: c.ink,
              }}
            />
            <div style={{ display: 'flex', gap: 4 }}>
              {(['1', '2', '5', '10']).map(q => (
                <button key={q} onClick={() => setQty(q)} style={{
                  padding: '5px 10px', borderRadius: 999,
                  background: qty === q ? c.ink : 'transparent', color: qty === q ? c.bg : c.inkSub,
                  border: `1px solid ${c.border}`, cursor: 'pointer',
                  fontSize: 11, fontFamily: theme.fontMono, fontWeight: 600,
                }}>{q}</button>
              ))}
            </div>
          </div>
        )}
      </div>

      {inline && suggChips}
      {inline && freqChips}

      {/* Portal a #root: el wrapper del SmartInput es position:absolute y crea
          stacking context — sin portal el Sheet quedaría atrapado dentro del
          tamaño del wrapper y se vería recortado. */}
      {ReactDOM.createPortal(
        <CategoryPicker theme={theme} app={app}
          open={catSheetOpen}
          onClose={() => setCatSheetOpen(false)}
          onPick={(catKey) => setManualCat(catKey)}
          selected={detectedCat} />,
        document.getElementById('root') || document.body,
      )}

      {bulk && ReactDOM.createPortal(
        <Sheet theme={theme} mode={app.isDesktop ? 'dialog' : 'sheet'} open
          onClose={() => setBulk(null)}
          title={t.bulkPasteTitle}>
          <p style={{
            margin: '0 0 12px', fontSize: 14, color: c.inkSub,
            fontFamily: theme.fontBody, lineHeight: 1.4,
          }}>{(t.bulkPasteQuestion || '').replace('{n}', bulk.lines.length)}</p>
          <div className="sl-scroll" style={{
            maxHeight: 240, overflow: 'auto',
            border: `1px solid ${c.border}`, borderRadius: theme.radius.md,
            padding: 8, marginBottom: 14, background: c.bgSoft || c.bg,
          }}>
            {bulk.lines.map((line, i) => {
              const catKey = (typeof autoCategoryFor === 'function') ? autoCategoryFor(line) : null;
              const cat = catKey ? CATEGORIES[catKey] : null;
              return (
                <div key={i} style={{
                  display: 'flex', alignItems: 'center', gap: 8, padding: '6px 4px',
                  fontSize: 14, fontFamily: theme.fontBody, color: c.ink,
                }}>
                  <span style={{ width: 22, textAlign: 'center' }}>{cat?.emoji || '·'}</span>
                  <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{line}</span>
                </div>
              );
            })}
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            <Btn theme={theme} variant="solid" size="md" full icon="plus"
              onClick={confirmBulk}>
              {(t.bulkPasteAdd || '').replace('{n}', bulk.lines.length)}
            </Btn>
            <Btn theme={theme} variant="ghost" size="md" full onClick={keepBulkAsOne}>
              {t.bulkPasteKeepOne || t.nope}
            </Btn>
          </div>
        </Sheet>,
        document.getElementById('root') || document.body,
      )}
    </div>
  );
}

// ─── Sheets: cambiar lista, nueva lista, unirme, compartir, frecuentes ───
// Extrae el token de un input que puede ser:
//  - URL nueva: http://host/#/s/TOKEN
//  - URL antigua: http://host/shared/TOKEN o http://host/s/TOKEN
//  - URL parcial: shop.jarsss8.es/s/TOKEN
//  - Solo el token: TOKEN
// Acepta cualquier formato de token (nuestro shortToken xxx-xxx-xxx u otros
// formatos legacy hexadecimales) — extrae todo lo que aparezca después de
// /s/ o /shared/ hasta un delimitador.
function extractToken(raw) {
  const s = (raw || '').trim();
  if (!s) return '';
  const tokenRe = /\/(?:s|shared)\/([^/?#&\s]+)/i;
  // URL parseable: mira hash, después pathname.
  try {
    const u = new URL(s);
    const hashM = u.hash.match(tokenRe);
    if (hashM) return hashM[1].toLowerCase();
    const pathM = u.pathname.match(tokenRe);
    if (pathM) return pathM[1].toLowerCase();
  } catch (_) { /* no era URL completa */ }
  // Búsqueda blanda: busca /s/X o /shared/X dentro del string
  const m = s.match(tokenRe);
  if (m) return m[1].toLowerCase();
  // Token pelado
  return s.toLowerCase();
}

// Sheet de unirse a lista: input para pegar enlace/token + opción QR scan
// (usando BarcodeDetector cuando está disponible — Chrome Android, Capacitor
// Android, escritorio. iOS Safari/WKWebView aún no lo soporta, así que en ese
// caso solo se muestra la opción de pegar).
function JoinSheet({ app, theme, onClose }) {
  const c = theme.c;
  const t = app.t;
  const [token, setToken]     = React.useState('');
  const [scanning, setScanning] = React.useState(false);
  const [error, setError]     = React.useState('');
  const [permDenied, setPermDenied] = React.useState(false);
  const [busy, setBusy]       = React.useState(false);
  const videoRef    = React.useRef(null);
  const streamRef   = React.useRef(null);
  const detectorRef = React.useRef(null);
  const rafRef      = React.useRef(0);
  // BarcodeDetector existe en Chromium (Chrome Android, Capacitor Android,
  // desktop). Para iOS Safari / WKWebView caemos a jsQR (UMD cargado en
  // index.html) que decodifica frames del <video> en software puro.
  const hasBarcodeDetector = typeof window !== 'undefined' && 'BarcodeDetector' in window;
  const hasJsQR = typeof window !== 'undefined' && typeof window.jsQR === 'function';
  const canScan = (hasBarcodeDetector || hasJsQR)
                  && typeof navigator !== 'undefined' && navigator.mediaDevices?.getUserMedia;
  const isCapacitor = typeof window !== 'undefined' && !!window.Capacitor?.isNativePlatform?.();

  const stopScan = React.useCallback(() => {
    cancelAnimationFrame(rafRef.current);
    if (streamRef.current) {
      streamRef.current.getTracks().forEach(tr => tr.stop());
      streamRef.current = null;
    }
    if (videoRef.current) {
      try { videoRef.current.srcObject = null; } catch (_) {}
    }
    detectorRef.current = null;
    setScanning(false);
  }, []);

  React.useEffect(() => () => stopScan(), [stopScan]);

  // Cuando React monta el <video> (scanning=true), enganchamos el stream y
  // arrancamos el loop de detección. Hacerlo aquí (no inline en startScan)
  // evita la race condition donde rAF dispara antes de que React haya
  // commiteado el <video> al DOM y srcObject se queda colgado.
  React.useEffect(() => {
    if (!scanning) return;
    const video = videoRef.current;
    const stream = streamRef.current;
    if (!video || !stream) return;
    let cancelled = false;
    video.srcObject = stream;
    const play = video.play();
    if (play && typeof play.catch === 'function') play.catch(() => {});

    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d', { willReadFrequently: true });
    const detectViaJsQR = () => {
      if (!video || video.readyState < 2 || !ctx) return null;
      const w = video.videoWidth, h = video.videoHeight;
      if (!w || !h) return null;
      const scale = Math.min(1, 480 / Math.max(w, h));
      canvas.width  = Math.round(w * scale);
      canvas.height = Math.round(h * scale);
      ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
      const img = ctx.getImageData(0, 0, canvas.width, canvas.height);
      const code = window.jsQR(img.data, img.width, img.height, { inversionAttempts: 'dontInvert' });
      return code?.data || null;
    };

    const tick = async () => {
      if (cancelled || !streamRef.current || !videoRef.current) return;
      try {
        let raw = null;
        if (detectorRef.current) {
          const codes = await detectorRef.current.detect(videoRef.current);
          if (codes && codes.length > 0) raw = codes[0].rawValue || '';
        } else if (hasJsQR) {
          raw = detectViaJsQR();
        }
        if (raw) {
          stopScan();
          doJoin(raw);
          return;
        }
      } catch (_) { /* sigue intentando */ }
      rafRef.current = requestAnimationFrame(tick);
    };
    rafRef.current = requestAnimationFrame(tick);
    return () => { cancelled = true; cancelAnimationFrame(rafRef.current); };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [scanning]);

  function openAppSettings() {
    // capacitor-native-settings expone NativeSettings.openIOS / openAndroid.
    // En iOS abre la página de Ajustes específica de la app; en Android abre
    // los Detalles de la aplicación (donde están los toggles de permisos).
    try {
      const NS = window.Capacitor?.Plugins?.NativeSettings;
      if (NS?.open) { NS.open({ optionIOS: 'app', optionAndroid: 'application_details' }); return; }
      if (NS?.openIOS && isCapacitor) {
        const platform = window.Capacitor?.getPlatform?.();
        if (platform === 'ios') NS.openIOS({ option: 'app' });
        else if (platform === 'android') NS.openAndroid({ option: 'application_details' });
        return;
      }
    } catch (_) {}
    // Fallback web: intenta el esquema iOS por si Capacitor delega.
    try { window.open('app-settings:', '_system'); } catch (_) {}
  }

  async function pasteFromClipboard() {
    setError('');
    // En Capacitor WKWebView `navigator.clipboard.readText()` falla a menudo
    // sin permiso explícito. Probamos primero el plugin nativo @capacitor/
    // clipboard, que lee el pasteboard del SO sin prompt, y caemos al Web API
    // para web.
    try {
      const CB = window.Capacitor?.Plugins?.Clipboard;
      if (CB?.read) {
        const { value } = await CB.read();
        setToken(value || '');
        return;
      }
    } catch (_) {}
    try {
      const txt = await navigator.clipboard.readText();
      setToken(txt || '');
    } catch (_) {
      setError(app.lang === 'es' ? 'No se pudo leer el portapapeles' : 'Could not read clipboard');
    }
  }

  async function doJoin(raw) {
    setError('');
    const tk = extractToken(raw || '');
    if (!tk) { setError(app.lang === 'es' ? 'Token inválido' : 'Invalid token'); return; }
    if (!window.SLDb?.findListByToken) {
      setError(app.lang === 'es' ? 'Sin conexión' : 'Offline'); return;
    }
    setBusy(true);
    try {
      const row = await window.SLDb.findListByToken(tk);
      if (!row) {
        setError(app.lang === 'es' ? 'Lista no encontrada' : 'List not found');
        return;
      }
      // Des-ocultar si estaba en la lista local de listas ocultas.
      if (app.hiddenListIds?.includes(row.id) && app.__internal_setHiddenListIds) {
        app.__internal_setHiddenListIds(prev => prev.filter(id => id !== row.id));
      }
      // Refrescar listas tras añadir el token al storage local.
      const adapters = window.__slAdapters;
      const rows = await window.SLDb.getLists();
      const hiddenAfter = new Set((app.hiddenListIds || []).filter(id => id !== row.id));
      const prevById = new Map(app.lists.map(l => [l.id, l]));
      const remoteLists = rows.filter(r => !hiddenAfter.has(r.id))
        .map(r => adapters.listDbToLocal(r, prevById.get(r.id)));
      app.__internal_setLists(remoteLists);
      app.__internal_setCurrentListId(row.id);
      onClose();
    } catch (e) {
      setError(app.lang === 'es' ? 'Error al unirse' : 'Failed to join');
    } finally {
      setBusy(false);
    }
  }

  async function startScan() {
    setError('');
    setPermDenied(false);
    if (!canScan) {
      setError(app.lang === 'es' ? 'Escáner no disponible aquí' : 'Scanner not available here');
      return;
    }
    try {
      if (hasBarcodeDetector) {
        detectorRef.current = new window.BarcodeDetector({ formats: ['qr_code'] });
      }
      const stream = await navigator.mediaDevices.getUserMedia({
        video: { facingMode: { ideal: 'environment' } }, audio: false,
      });
      streamRef.current = stream;
      // El attach del stream + arranque del tick loop pasa en el useEffect
      // que reacciona a `scanning`, garantizando que <video> esté en DOM.
      setScanning(true);
    } catch (e) {
      detectorRef.current = null;
      // iOS no re-pregunta tras un deny; el usuario tiene que ir a Ajustes →
      // KomprApp → Cámara. Detectamos NotAllowedError (Chromium / iOS) y la
      // variante SecurityError (algunos WebViews) para enseñar el botón de
      // abrir Ajustes en vez del mensaje genérico.
      const denied = e?.name === 'NotAllowedError' || e?.name === 'SecurityError';
      if (denied) {
        setPermDenied(true);
        setError('');
      } else {
        setError(app.lang === 'es' ? 'No se pudo acceder a la cámara' : 'Could not access camera');
      }
      setScanning(false);
    }
  }

  return (
    <Sheet theme={theme} mode={app.isDesktop ? 'dialog' : 'sheet'} open
      onClose={() => { stopScan(); onClose(); }}
      title={t.join}>
      {scanning ? (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          <div style={{
            position: 'relative', width: '100%', aspectRatio: '1 / 1',
            background: '#000', borderRadius: theme.radius.lg, overflow: 'hidden',
          }}>
            <video ref={videoRef} playsInline muted autoPlay
              style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
            <div style={{
              position: 'absolute', inset: '15%',
              border: `2px solid ${c.accent}`,
              borderRadius: theme.radius.md, pointerEvents: 'none',
              boxShadow: '0 0 0 9999px rgba(0,0,0,0.35)',
            }} />
          </div>
          <Btn theme={theme} variant="ghost" size="md" full icon="x" onClick={stopScan}>
            {app.lang === 'es' ? 'Cancelar' : 'Cancel'}
          </Btn>
        </div>
      ) : (
        <>
          <div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
            <input autoFocus value={token} onChange={e => setToken(e.target.value)}
              onKeyDown={e => { if (e.key === 'Enter' && token.trim()) doJoin(token); }}
              placeholder={t.joinPaste}
              style={{
                flex: 1, minWidth: 0, padding: '14px 16px', fontSize: 14, fontFamily: theme.fontMono,
                border: `1.5px solid ${c.border}`,
                borderRadius: theme.radius.lg, outline: 'none',
                background: c.bg, color: c.ink, boxSizing: 'border-box',
              }} />
            <button type="button" onClick={pasteFromClipboard}
              aria-label={app.lang === 'es' ? 'Pegar' : 'Paste'}
              title={app.lang === 'es' ? 'Pegar' : 'Paste'}
              style={{
                width: 48, flexShrink: 0, borderRadius: theme.radius.lg,
                border: `1.5px solid ${c.border}`, background: c.bg,
                color: c.inkSub, cursor: 'pointer',
                display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
              }}>
              <Icon name="copy" size={18} stroke={2.2} />
            </button>
          </div>
          <Btn theme={theme} variant="ghost" size="md" full icon="qr"
            onClick={startScan} disabled={!canScan}
            style={{ marginBottom: 10 }}>
            {canScan
              ? (app.lang === 'es' ? 'Escanear QR' : 'Scan QR')
              : (app.lang === 'es' ? 'QR no disponible' : 'QR not available')}
          </Btn>
          {permDenied && (
            <div style={{
              padding: '10px 12px', marginBottom: 10,
              borderRadius: theme.radius.md, border: `1px solid ${c.border}`,
              background: c.bgAlt, color: c.ink, fontSize: 13,
              fontFamily: theme.fontBody, lineHeight: 1.45,
            }}>
              <div style={{ marginBottom: 8 }}>
                {app.lang === 'es'
                  ? 'Permiso de cámara denegado. Actívalo desde Ajustes para escanear.'
                  : 'Camera permission denied. Enable it in Settings to scan.'}
              </div>
              <Btn theme={theme} variant="solid" size="sm" full icon="arrow-right" onClick={openAppSettings}>
                {app.lang === 'es' ? 'Abrir Ajustes' : 'Open Settings'}
              </Btn>
            </div>
          )}
          {error && (
            <div style={{ color: c.danger, fontSize: 13, marginBottom: 10, fontFamily: theme.fontBody }}>{error}</div>
          )}
          <Btn theme={theme} variant="solid" size="md" full icon="arrow-right"
            disabled={!token.trim() || busy}
            onClick={() => doJoin(token)}>
            {busy ? '…' : t.join}
          </Btn>
        </>
      )}
    </Sheet>
  );
}

function AppSheets({ app, theme, sheet, setSheet }) {
  const c = theme.c;
  const t = app.t;
  const [name, setName]   = React.useState('');
  const [token, setToken] = React.useState('');
  const [copied, setCopied] = React.useState(false);
  const colorMap = { lime: '#D4FF3A', pink: '#FF3D8B', cyan: '#5EEAD4', orange: '#FDBA74', violet: '#A78BFA' };

  React.useEffect(() => {
    // Pre-rellena el input de rename con el nombre actual; resto de sheets vacío.
    if (sheet === 'rename' && app.list) {
      setName(app.list.name[app.lang] || '');
    } else {
      setName('');
    }
    setToken('');
  }, [sheet, app.list?.id, app.lang]);

  if (!sheet) return null;

  if (sheet === 'lists') {
    return (
      <Sheet theme={theme} mode={app.isDesktop ? 'dialog' : 'sheet'} open onClose={() => setSheet(null)} title={app.lang === 'es' ? 'Cambiar de lista' : 'Switch list'}>
        {/* Acciones rápidas arriba: nueva lista o unirse con código/QR. */}
        <div style={{ display: 'flex', gap: 8, marginBottom: 14 }}>
          <Btn theme={theme} variant="solid" size="md" full icon="plus" onClick={() => setSheet('newList')}>{t.newList}</Btn>
          <Btn theme={theme} variant="ghost" size="md" full icon="qr" onClick={() => setSheet('join')}>{t.join}</Btn>
        </div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {app.lists.map(l => (
            <button key={l.id} onClick={() => { app.setCurrentListId(l.id); setSheet(null); }}
              style={{
                display: 'flex', alignItems: 'center', gap: 12, padding: '14px 14px',
                background: l.id === app.currentListId ? c.accent : c.bgCard,
                color: l.id === app.currentListId ? c.accentInk : c.ink,
                border: `1px solid ${c.border}`,
                borderRadius: theme.radius.lg, cursor: 'pointer', textAlign: 'left',
                fontFamily: theme.fontDisplay,
              }}>
              <span style={{
                width: 36, height: 36, borderRadius: '50%',
                background: colorMap[l.color] || c.accent,
                display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                fontWeight: 800, color: '#0E0E0C',
              }}>{l.name[app.lang][0]}</span>
              <span style={{ flex: 1, fontWeight: 700, fontSize: 16 }}>{l.name[app.lang]}</span>
              {l.id === app.currentListId && <Icon name="check" size={18} stroke={2.6} />}
            </button>
          ))}
        </div>
      </Sheet>
    );
  }

  if (sheet === 'newList') {
    return (
      <Sheet theme={theme} mode={app.isDesktop ? 'dialog' : 'sheet'} open onClose={() => setSheet(null)} title={t.newList}>
        <input autoFocus value={name} onChange={e => setName(e.target.value)}
          onKeyDown={e => { if (e.key === 'Enter' && name.trim()) { app.createList(name); setSheet(null); }}}
          placeholder={t.listName}
          style={{
            width: '100%', padding: '14px 16px', fontSize: 16, fontFamily: theme.fontDisplay,
            border: `1.5px solid ${c.border}`,
            borderRadius: theme.radius.lg, outline: 'none',
            background: c.bg, color: c.ink, marginBottom: 12, boxSizing: 'border-box',
          }} />
        <Btn theme={theme} variant="solid" size="md" full disabled={!name.trim()}
          onClick={() => { app.createList(name); setSheet(null); }}>{t.create}</Btn>
      </Sheet>
    );
  }

  if (sheet === 'join') {
    return <JoinSheet app={app} theme={theme} onClose={() => setSheet(null)} />;
  }

  if (sheet === 'share') {
    if (!app.list) { setSheet(null); return null; }
    const link = `${SHARE_ORIGIN}/#/s/${app.list.token}`;
    const doShare = async () => {
      // Incluimos el link en `text` además de `url` para que cualquier acción
      // "Copiar" del share sheet de iOS termine con la URL en el portapapeles
      // (a veces WKWebView ignora el campo `url` y copia solo `text`).
      if (navigator.share) {
        try { await navigator.share({ title: app.list.name[app.lang], text: link, url: link }); return; } catch (e) {}
      }
      try { await navigator.clipboard.writeText(link); } catch (e) {}
      setCopied(true); setTimeout(() => setCopied(false), 1800);
    };
    return (
      <Sheet theme={theme} mode={app.isDesktop ? 'dialog' : 'sheet'} open onClose={() => setSheet(null)} title={`${t.share} · ${app.list.name[app.lang]}`}>
        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 14, marginBottom: 16 }}>
          <FakeQR theme={theme} value={link} size={180} />
          <div style={{ fontSize: 13, fontFamily: theme.fontMono, color: c.inkSub, textAlign: 'center' }}>{link}</div>
          <Btn theme={theme} variant={copied ? 'solid' : 'ghost'} size="md" full icon={copied ? 'check' : 'share'}
            onClick={doShare}>
            {copied ? t.copied : (navigator.share ? t.share : t.copyLink)}
          </Btn>
          <p style={{ fontSize: 12, color: c.inkMuted, textAlign: 'center', fontFamily: theme.fontBody, lineHeight: 1.4, margin: 0 }}>{t.shareNote}</p>
        </div>
      </Sheet>
    );
  }

  if (sheet === 'rename') {
    if (!app.list) { setSheet(null); return null; }
    const submit = () => {
      const trimmed = name.trim();
      if (!trimmed) return;
      app.renameList(app.list.id, trimmed);
      setSheet(null);
    };
    return (
      <Sheet theme={theme} mode={app.isDesktop ? 'dialog' : 'sheet'} open onClose={() => setSheet(null)}
        title={app.lang === 'es' ? 'Renombrar lista' : 'Rename list'}>
        <input autoFocus value={name} onChange={e => setName(e.target.value)}
          onKeyDown={e => { if (e.key === 'Enter') submit(); }}
          placeholder={t.listName}
          style={{
            width: '100%', padding: '14px 16px', fontSize: 16, fontFamily: theme.fontDisplay,
            border: `1.5px solid ${c.border}`,
            borderRadius: theme.radius.lg, outline: 'none',
            background: c.bg, color: c.ink, marginBottom: 12, boxSizing: 'border-box',
          }} />
        <Btn theme={theme} variant="solid" size="md" full disabled={!name.trim()} onClick={submit}>
          {app.lang === 'es' ? 'Guardar' : 'Save'}
        </Btn>
      </Sheet>
    );
  }

  if (sheet === 'deleteList') {
    if (!app.list) { setSheet(null); return null; }
    const onlyOne = app.lists.length <= 1;
    return (
      <Sheet theme={theme} mode={app.isDesktop ? 'dialog' : 'sheet'} open onClose={() => setSheet(null)}
        title={app.lang === 'es' ? 'Eliminar lista' : 'Delete list'}>
        <p style={{ fontFamily: theme.fontBody, fontSize: 14, color: c.inkSub, lineHeight: 1.5, marginTop: 0 }}>
          {onlyOne
            ? (app.lang === 'es'
                ? 'Es tu única lista. Crea otra antes de eliminarla.'
                : 'This is your only list. Create another one before deleting.')
            : (app.lang === 'es'
                ? `Vas a eliminar "${app.list.name[app.lang]}" y todos sus productos. Esta acción no se puede deshacer.`
                : `You will delete "${app.list.name[app.lang]}" and all its items. This cannot be undone.`)}
        </p>
        <div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
          <Btn theme={theme} variant="ghost" size="md" full onClick={() => setSheet(null)}>
            {app.lang === 'es' ? 'Cancelar' : 'Cancel'}
          </Btn>
          <Btn theme={theme} variant="solid" size="md" full disabled={onlyOne}
            style={{ background: c.danger, color: '#fff', borderColor: c.danger }}
            onClick={() => { app.deleteList(app.list.id); setSheet(null); }}>
            {app.lang === 'es' ? 'Eliminar' : 'Delete'}
          </Btn>
        </div>
      </Sheet>
    );
  }

  if (sheet === 'sort') {
    const ts = t.sort || {};
    const opts = [
      { key: 'added', label: ts.byAdded, icon: 'history' },
      { key: 'name',  label: ts.byName,  icon: 'list' },
    ];
    return (
      <Sheet theme={theme} mode={app.isDesktop ? 'dialog' : 'sheet'} open onClose={() => setSheet(null)} title={ts.title}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 14 }}>
          {opts.map(o => {
            const active = app.sortMode === o.key;
            return (
              <button key={o.key} onClick={() => app.setSortMode(o.key)}
                style={{
                  display: 'flex', alignItems: 'center', gap: 12, padding: '14px 14px',
                  background: active ? c.accent : c.bgCard,
                  color: active ? c.accentInk : c.ink,
                  border: `1px solid ${active ? c.accent : c.border}`,
                  borderRadius: theme.radius.lg, cursor: 'pointer', textAlign: 'left',
                  fontFamily: theme.fontDisplay, fontSize: 15, fontWeight: 700,
                }}>
                <Icon name={o.icon} size={18} stroke={2.4} />
                <span style={{ flex: 1 }}>{o.label}</span>
                {active && <Icon name="check" size={18} stroke={2.6} />}
              </button>
            );
          })}
        </div>
        <button onClick={() => app.setGroupByCategory(!app.groupByCategory)}
          style={{
            display: 'flex', alignItems: 'center', gap: 12, padding: '14px 14px', width: '100%',
            background: app.groupByCategory ? c.accent : c.bgCard,
            color: app.groupByCategory ? c.accentInk : c.ink,
            border: `1px solid ${app.groupByCategory ? c.accent : c.border}`,
            borderRadius: theme.radius.lg, cursor: 'pointer', textAlign: 'left',
            fontFamily: theme.fontDisplay, fontSize: 15, fontWeight: 700,
          }}>
          <Icon name="stack" size={18} stroke={2.4} />
          <span style={{ flex: 1 }}>{ts.groupByCategory}</span>
          <span style={{
            width: 38, height: 22, borderRadius: 999,
            background: app.groupByCategory ? c.accentInk : c.border,
            position: 'relative', flexShrink: 0,
            transition: 'background .18s',
          }}>
            <span style={{
              position: 'absolute', top: 2, left: app.groupByCategory ? 18 : 2,
              width: 18, height: 18, borderRadius: '50%',
              background: app.groupByCategory ? c.accent : c.bgCard,
              transition: 'left .18s',
            }} />
          </span>
        </button>
      </Sheet>
    );
  }

  if (sheet === 'frequent') {
    return (
      <Sheet theme={theme} mode={app.isDesktop ? 'dialog' : 'sheet'} open onClose={() => setSheet(null)} title={t.quickAdd}>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
          {(app.frequents || FREQUENT[app.lang]).map(f => (
            <button key={f} onClick={() => { app.addItem(f); setSheet(null); }}
              style={{
                padding: '12px 18px', borderRadius: 999,
                background: c.bgCard, color: c.ink,
                border: `1px solid ${c.border}`,
                fontSize: 14, fontWeight: 700, fontFamily: theme.fontDisplay,
                cursor: 'pointer',
              }}>+ {f}</button>
          ))}
        </div>
      </Sheet>
    );
  }

  return null;
}

window.SmartInput = SmartInput;
window.AppSheets = AppSheets;
