/* global React, window */
// Ruticomidas — modal system.
// Components listen for window 'rc:modal' CustomEvent with detail = { name, props }
// Dispatching with name: null closes the modal.

const { useState, useEffect, useMemo, useRef } = React;
const D = window.RC_DATA;

// =========================================================
// Shell
// =========================================================
function ModalShell({ kicker, title, size, children, foot, onClose }) {
  useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    document.body.style.overflow = 'hidden';
    return () => {
      document.removeEventListener('keydown', onKey);
      document.body.style.overflow = '';
    };
  }, [onClose]);

  const onBackdrop = (e) => { if (e.target === e.currentTarget) onClose(); };

  return (
    <div className="modal-backdrop" onClick={onBackdrop}>
      <div className={'modal' + (size === 'wide' ? ' modal--wide' : '') + (size === 'xl' ? ' modal--xl' : '')}>
        <div className="modal__head">
          <div>
            {kicker && <div className="kicker">{kicker}</div>}
            <h2>{title}</h2>
          </div>
          <button className="modal__close" onClick={onClose} aria-label="cerrar">✕</button>
        </div>
        <div className="modal__body">{children}</div>
        {foot && <div className="modal__foot">{foot}</div>}
      </div>
    </div>
  );
}

const Field = ({ label, full, children }) => (
  <div className={'field' + (full ? ' full' : '')}>
    <label>{label}</label>
    {children}
  </div>
);

const Toggle = ({ checked, onChange, label }) => (
  <label className="toggle">
    <input type="checkbox" checked={checked} onChange={(e) => onChange && onChange(e.target.checked)} />
    <span className="track" />
    <span className="lbl">{label}</span>
  </label>
);

// =========================================================
// MEAL — assign recipe + loose ingredients to a planning slot
// =========================================================
function MealModal({ day, slot, recipes, onSave, onClose }) {
  const recipeList = recipes || [];
  const [selectedId, setSelectedId] = useState('');
  const [busy, setBusy] = useState(false);

  const planDate = day?.date;
  const mealType = slot?.id;

  async function doSave() {
    if (!selectedId) { window.showToast('Elige una receta', 'error'); return; }
    if (!planDate || !mealType) { window.showToast('Datos de planificación inválidos', 'error'); return; }
    setBusy(true);
    try {
      await window.api(`/api/planning/${planDate}/${mealType}/recipes`, 'POST', {
        recipe_id: parseInt(selectedId, 10),
        raciones: 1,
      });
      if (onSave) await onSave();
      onClose();
    } catch (e) {
      window.showToast(e.message, 'error');
    } finally { setBusy(false); }
  }

  return (
    <ModalShell
      kicker={`${(day?.dow || '').toUpperCase()} ${day?.dom || ''} · ${(slot?.label || '').toUpperCase()}`}
      title="Añadir receta"
      size="wide"
      onClose={onClose}
      foot={
        <div className="right">
          <button className="btn" onClick={onClose} disabled={busy}>Cancelar</button>
          <button className="btn btn--primary" onClick={doSave} disabled={busy}>{busy ? '…' : 'Añadir'}</button>
        </div>
      }
    >
      <div className="fgrid">
        <Field full label="RECETA">
          <select className="select" value={selectedId} onChange={(e) => setSelectedId(e.target.value)}>
            <option value="">— Selecciona receta —</option>
            {recipeList.map(r => <option key={r.id} value={String(r.id)}>{r.name}</option>)}
          </select>
        </Field>
      </div>
      <div className="t-mono-micro dim" style={{ marginTop: 8 }}>
        ● EL STOCK SE DESCUENTA AUTOMÁTICAMENTE AL FINALIZAR EL DÍA
      </div>
    </ModalShell>
  );
}

// =========================================================
// LOOSE — quick add of a single loose ingredient
// =========================================================
function LooseModal({ day, slot, onSave, onClose }) {
  const { ingredients } = window.RC_STORE.useStore();
  const sortedIngs = [...(ingredients || [])].sort((a, b) => a.name.localeCompare(b.name));
  const [name, setName] = useState('');
  const [qty, setQty] = useState(1);
  const [unit, setUnit] = useState('ud');
  const [busy, setBusy] = useState(false);

  const planDate = day?.date;
  const mealType = slot?.id;

  async function doAdd() {
    if (!name.trim()) { window.showToast('Nombre requerido', 'error'); return; }
    if (!planDate || !mealType) { window.showToast('Datos inválidos', 'error'); return; }
    setBusy(true);
    try {
      await window.api(`/api/planning/${planDate}/${mealType}/loose`, 'POST', {
        ingredient_name: name.trim(), qty: parseFloat(qty) || 1, unit,
      });
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  return (
    <ModalShell
      kicker={`${(day?.dow || '').toUpperCase()} ${day?.dom || ''} · ${(slot?.label || '').toUpperCase()}`}
      title="Ingrediente suelto"
      onClose={onClose}
      foot={
        <>
          <span className="t-mono-micro dim">EXTRA FUERA DE RECETA</span>
          <div className="right">
            <button className="btn" onClick={onClose} disabled={busy}>Cancelar</button>
            <button className="btn btn--primary" onClick={doAdd} disabled={busy}>{busy ? '…' : 'Añadir'}</button>
          </div>
        </>
      }
    >
      <div className="fgrid">
        <Field full label="INGREDIENTE">
          <select className="select" autoFocus value={name} onChange={(e) => setName(e.target.value)}>
            <option value="">— Selecciona —</option>
            {sortedIngs.map(ing => <option key={ing.id} value={ing.name}>{ing.name}</option>)}
          </select>
        </Field>
        <Field label="CANTIDAD">
          <input className="input" type="number" value={qty} onChange={(e) => setQty(+e.target.value)} />
        </Field>
        <Field label="UNIDAD">
          <select className="select" value={unit} onChange={(e) => setUnit(e.target.value)}>
            <option>ud</option><option>g</option><option>kg</option><option>ml</option><option>L</option><option>cda</option>
          </select>
        </Field>
      </div>
    </ModalShell>
  );
}

// =========================================================
// RECIPE — view/edit a recipe
// =========================================================
function RecipeModal({ recipe, edit: startEdit, onSave, onClose }) {
  const { planningWeek, ingredients, reload } = window.RC_STORE.useStore();
  const [mode, setMode] = useState(startEdit ? 'edit' : 'view'); // 'view' | 'edit' | 'plan'
  const [busy, setBusy] = useState(false);
  const [newIngTarget, setNewIngTarget] = useState(null);

  // edit form state
  const [name, setName] = useState(recipe?.name || '');
  const [kinds, setKinds] = useState(() => (recipe?.kinds || recipe?.meal_types || []).map(k => {
    const m = { desayuno: 'Desayuno', comida: 'Comida', merienda: 'Merienda', cena: 'Cena' };
    return m[k] || (typeof k === 'string' ? k.charAt(0).toUpperCase() + k.slice(1) : k);
  }));
  const [notes, setNotes] = useState(recipe?.notes || '');
  const [isPublic, setIsPublic] = useState(!!recipe?.is_public);
  const [ings, setIngs] = useState(() => (recipe?.ingredients || []).map(i => ({ ...i })));

  // planning picker state
  const [planDate, setPlanDate] = useState('');
  const [planSlot, setPlanSlot] = useState('desayuno');
  const [planRaciones, setPlanRaciones] = useState(2);

  const sortedIngs = [...(ingredients || [])].sort((a, b) => a.name.localeCompare(b.name));

  const toggleKind = (k) => setKinds(kk => kk.includes(k) ? kk.filter(x => x !== k) : [...kk, k]);
  const addIng = () => setIngs(l => [...l, { name: '', qty: '', unit: 'ud' }]);
  const updIng = (i, k, v) => setIngs(l => l.map((row, j) => j === i ? { ...row, [k]: v } : row));
  const delIng = (i) => setIngs(l => l.filter((_, j) => j !== i));

  function handleIngName(i, v) {
    if (v === '__new__') { setNewIngTarget(i); }
    else { updIng(i, 'name', v); }
  }

  async function doDelete() {
    if (!recipe?.id) return;
    setBusy(true);
    try {
      await window.api(`/api/recipes/${recipe.id}`, 'DELETE');
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  async function doSaveEdit() {
    if (!name.trim()) { window.showToast('Nombre requerido', 'error'); return; }
    const validNames = new Set(sortedIngs.map(ing => ing.name.toLowerCase()));
    const invalidIng = ings.filter(r => r.name?.trim()).find(r => !validNames.has(r.name.trim().toLowerCase()));
    if (invalidIng) { window.showToast(`Ingrediente no válido: ${invalidIng.name}`, 'error'); return; }
    setBusy(true);
    try {
      await window.api(`/api/recipes/${recipe.id}`, 'PUT', {
        name: name.trim(),
        notes,
        meal_types: kinds.map(k => k.toLowerCase()),
        ingredients: ings.filter(r => r.name?.trim()).map(r => ({
          name: r.name.trim(), qty: parseFloat(r.qty) || 1, unit: r.unit,
        })),
        is_public: isPublic,
      });
      window.showToast('Receta guardada');
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  async function doAddPlanning() {
    if (!planDate || !planSlot) { window.showToast('Elige día y comida', 'error'); return; }
    setBusy(true);
    try {
      await window.api(`/api/planning/${planDate}/${planSlot}/recipes`, 'POST', {
        recipe_id: recipe.id, raciones: planRaciones,
      });
      window.showToast('Añadido al planning');
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  const viewIngs = recipe?.ingredients || [];
  const viewKinds = recipe?.kinds || recipe?.meal_types || [];

  if (mode === 'edit') {
    return (
      <>
        {newIngTarget !== null && (
          <NewIngredientOverlay
            onCreated={(ingName) => { updIng(newIngTarget, 'name', ingName); setNewIngTarget(null); }}
            onClose={() => setNewIngTarget(null)}
          />
        )}
        <ModalShell
          kicker="RECETA · EDITANDO"
          title={name || recipe?.name || ''}
          size="wide"
          onClose={onClose}
          foot={
            <>
              <button className="btn btn--ghost btn--sm" style={{ color: 'var(--tile-pink)' }} onClick={doDelete} disabled={busy}>Borrar</button>
              <div className="right">
                <button className="btn" onClick={() => setMode('view')} disabled={busy}>Cancelar</button>
                <button className="btn btn--primary" onClick={doSaveEdit} disabled={busy}>{busy ? '…' : 'Guardar'}</button>
              </div>
            </>
          }
        >
          <div className="fgrid">
            <Field full label="NOMBRE">
              <input className="input" autoFocus value={name} onChange={e => setName(e.target.value)} />
            </Field>
            <Field full label="TIPOS DE COMIDA">
              <div className="recipe-filters" style={{ marginTop: 4 }}>
                {['Desayuno', 'Comida', 'Merienda', 'Cena'].map(k => (
                  <button key={k} className={'chip' + (kinds.includes(k) ? ' active' : '')} onClick={() => toggleKind(k)} type="button">{k}</button>
                ))}
              </div>
            </Field>
            <Field full label="NOTAS">
              <textarea className="input" rows="2" value={notes} onChange={e => setNotes(e.target.value)} style={{ width: '100%', resize: 'vertical' }} />
            </Field>
          </div>
          <div className="t-mono-label dim" style={{ marginBottom: 8 }}>INGREDIENTES</div>
          {ings.map((row, i) => (
            <div key={i} style={{ display: 'flex', gap: 8, marginBottom: 6, alignItems: 'center' }}>
              <select className="select" style={{ flex: 3 }} value={row.name || ''} onChange={e => handleIngName(i, e.target.value)}>
                <option value="">— Selecciona —</option>
                {row.name && !sortedIngs.find(ing => ing.name === row.name) && (
                  <option value={row.name}>{row.name} ⚠</option>
                )}
                {sortedIngs.map(ing => <option key={ing.id} value={ing.name}>{ing.name}</option>)}
                <option value="__new__">+ Nuevo artículo…</option>
              </select>
              <input className="input" style={{ flex: 1 }} type="number" placeholder="Qty" value={row.qty || ''} onChange={e => updIng(i, 'qty', e.target.value)} />
              <select className="select" style={{ flex: 1 }} value={row.unit || 'ud'} onChange={e => updIng(i, 'unit', e.target.value)}>
                <option>ud</option><option>g</option><option>kg</option><option>ml</option><option>L</option><option>cda</option>
              </select>
              <button className="btn btn--ghost btn--sm" style={{ color: 'var(--tile-pink)' }} onClick={() => delIng(i)}>✕</button>
            </div>
          ))}
          <button className="btn btn--outline btn--sm" onClick={addIng} style={{ marginTop: 4 }}>+ Ingrediente</button>
          <div style={{ marginTop: 16 }}>
            <Toggle checked={isPublic} onChange={setIsPublic} label="Receta pública" />
          </div>
        </ModalShell>
      </>
    );
  }

  if (mode === 'plan') {
    const days = planningWeek?.days || [];
    return (
      <ModalShell
        kicker="PLANNING · AÑADIR RECETA"
        title={recipe?.name || ''}
        size="wide"
        onClose={onClose}
        foot={
          <div className="right">
            <button className="btn" onClick={() => setMode('view')} disabled={busy}>Cancelar</button>
            <button className="btn btn--primary" onClick={doAddPlanning} disabled={busy}>{busy ? '…' : 'Añadir'}</button>
          </div>
        }
      >
        <div className="fgrid">
          <Field full label="DÍA">
            <select className="select" value={planDate} onChange={e => setPlanDate(e.target.value)}>
              <option value="">— elegir día —</option>
              {days.map(d => (
                <option key={d.date} value={d.date}>{d.dow} {d.dom}{d.today ? ' (hoy)' : ''}</option>
              ))}
            </select>
          </Field>
          <Field full label="COMIDA">
            <select className="select" value={planSlot} onChange={e => setPlanSlot(e.target.value)}>
              {(D.MEAL_SLOTS || []).map(s => <option key={s.id} value={s.id}>{s.label}</option>)}
            </select>
          </Field>
          <Field label="RACIONES">
            <input className="input" type="number" min="1" max="20" value={planRaciones} onChange={e => setPlanRaciones(+e.target.value)} />
          </Field>
        </div>
      </ModalShell>
    );
  }

  // view mode
  return (
    <ModalShell
      kicker={viewKinds.join(' · ').toUpperCase() + (recipe?.is_public ? ' · PÚBLICA' : ' · PRIVADA')}
      title={recipe?.name || ''}
      size="wide"
      onClose={onClose}
      foot={
        <>
          <button className="btn btn--ghost btn--sm" style={{ color: 'var(--tile-pink)' }} onClick={doDelete} disabled={busy}>Borrar</button>
          <div className="right">
            <button className="btn btn--ghost btn--sm" onClick={() => setMode('edit')} disabled={busy}>Editar</button>
            <button className="btn btn--primary" onClick={() => setMode('plan')} disabled={busy}>Añadir a planning</button>
          </div>
        </>
      }
    >
      <div className="row-between" style={{ marginTop: 4 }}>
        <div className="t-mono-label dim">INGREDIENTES · {viewIngs.length}</div>
      </div>

      <div style={{ border: '1px solid var(--frame)', borderRadius: 12, padding: '4px 14px' }}>
        {viewIngs.map((g, i) => (
          <div className="ing-row" key={i}>
            <span className="t-body-bold">{g.name}</span>
            <span className="t-mono-label dim" style={{ textAlign: 'right' }}>{g.qty}</span>
            <span className="t-mono-micro dim">{g.unit}</span>
            <span></span>
          </div>
        ))}
        {viewIngs.length === 0 && (
          <div style={{ color: 'var(--text-meta)', fontSize: 13, padding: '12px 0', textAlign: 'center' }}>Sin ingredientes definidos.</div>
        )}
      </div>

      {recipe?.notes && (
        <div>
          <div className="t-mono-label dim" style={{ marginBottom: 6 }}>NOTAS</div>
          <div className="t-serif" style={{ color: 'var(--text-muted)' }}>{recipe.notes}</div>
        </div>
      )}
    </ModalShell>
  );
}

// =========================================================
// NEW INGREDIENT OVERLAY (reutilizable desde modales de receta)
// =========================================================
function NewIngredientOverlay({ onCreated, onClose }) {
  const { reload } = window.RC_STORE.useStore();
  const [ingName, setIngName] = useState('');
  const [ingUnit, setIngUnit] = useState('ud');
  const [ingQty, setIngQty] = useState('1');
  const [ingSup, setIngSup] = useState('Mercadona');
  const [busy, setBusy] = useState(false);

  useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') { e.stopPropagation(); onClose(); } };
    document.addEventListener('keydown', onKey, true);
    return () => document.removeEventListener('keydown', onKey, true);
  }, [onClose]);

  async function doCreate() {
    if (!ingName.trim()) { window.showToast('Nombre requerido', 'error'); return; }
    setBusy(true);
    try {
      await window.api('/api/ingredients', 'POST', {
        name: ingName.trim(), unit: ingUnit, envase_qty: parseFloat(ingQty) || 1,
        supermarket: ingSup || 'Mercadona', stock: 0, min_stock: 0, obs: '', variable_weight: false,
      });
      await reload.ingredients();
      onCreated(ingName.trim());
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  return (
    <div style={{ position: 'fixed', inset: 0, zIndex: 900, background: 'rgba(0,0,0,0.55)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
      onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div style={{ background: 'var(--bg-card)', borderRadius: 16, padding: 24, width: 320, maxWidth: '90vw', display: 'flex', flexDirection: 'column', gap: 12 }}>
        <div className="kicker">NUEVO ARTÍCULO</div>
        <input className="input" autoFocus placeholder="Nombre del ingrediente" value={ingName}
          onChange={e => setIngName(e.target.value)}
          onKeyDown={e => { if (e.key === 'Enter' && !busy) doCreate(); }} />
        <div style={{ display: 'flex', gap: 8 }}>
          <input className="input" type="number" min="0.01" step="0.01" placeholder="Qty envase" value={ingQty}
            onChange={e => setIngQty(e.target.value)} style={{ flex: 1 }} />
          <select className="select" value={ingUnit} onChange={e => setIngUnit(e.target.value)} style={{ flex: 1 }}>
            <option>ud</option><option>g</option><option>kg</option><option>ml</option><option>L</option><option>cda</option>
          </select>
        </div>
        <input className="input" placeholder="Supermercado" value={ingSup} onChange={e => setIngSup(e.target.value)} />
        <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 4 }}>
          <button className="btn" onClick={onClose} disabled={busy}>Cancelar</button>
          <button className="btn btn--primary" onClick={doCreate} disabled={busy}>{busy ? '…' : 'Crear'}</button>
        </div>
      </div>
    </div>
  );
}

// =========================================================
// NEW RECIPE
// =========================================================
function NewRecipeModal({ onSave, onClose }) {
  const { ingredients, reload } = window.RC_STORE.useStore();
  const [name, setName] = useState('');
  const [kinds, setKinds] = useState(['Comida']);
  const [notes, setNotes] = useState('');
  const [isPublic, setIsPublic] = useState(false);
  const [ings, setIngs] = useState([{ name: '', qty: '', unit: 'g' }]);
  const [busy, setBusy] = useState(false);
  const [newIngTarget, setNewIngTarget] = useState(null);

  const sortedIngs = [...(ingredients || [])].sort((a, b) => a.name.localeCompare(b.name));

  const toggleKind = (k) => setKinds(kk => kk.includes(k) ? kk.filter(x => x !== k) : [...kk, k]);
  const addIng = () => setIngs(l => [...l, { name: '', qty: '', unit: 'g' }]);
  const updIng = (i, k, v) => setIngs(l => l.map((row, j) => j === i ? { ...row, [k]: v } : row));
  const delIng = (i) => setIngs(l => l.filter((_, j) => j !== i));

  function handleIngName(i, v) {
    if (v === '__new__') { setNewIngTarget(i); }
    else { updIng(i, 'name', v); }
  }

  async function doCreate() {
    if (!name.trim()) { window.showToast('Nombre requerido', 'error'); return; }
    const validNames = new Set(sortedIngs.map(ing => ing.name.toLowerCase()));
    const invalidIng = ings.filter(r => r.name.trim()).find(r => !validNames.has(r.name.trim().toLowerCase()));
    if (invalidIng) { window.showToast(`Ingrediente no válido: ${invalidIng.name}`, 'error'); return; }
    setBusy(true);
    try {
      await window.api('/api/recipes', 'POST', {
        name: name.trim(),
        notes,
        meal_types: kinds.map(k => k.toLowerCase()),
        ingredients: ings.filter(r => r.name.trim()).map(r => ({
          name: r.name.trim(), qty: parseFloat(r.qty) || 1, unit: r.unit,
        })),
        is_public: isPublic,
      });
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  return (
    <>
      {newIngTarget !== null && (
        <NewIngredientOverlay
          onCreated={(ingName) => { updIng(newIngTarget, 'name', ingName); setNewIngTarget(null); }}
          onClose={() => setNewIngTarget(null)}
        />
      )}
      <ModalShell
        kicker="COMIDAS · NUEVA RECETA"
        title="Nueva receta"
        size="wide"
        onClose={onClose}
        foot={
          <>
            <span className="t-mono-micro dim">SIN FOTO POR AHORA · PRÓXIMAMENTE</span>
            <div className="right">
              <button className="btn" onClick={onClose} disabled={busy}>Cancelar</button>
              <button className="btn btn--primary" onClick={doCreate} disabled={busy}>{busy ? '…' : 'Crear receta'}</button>
            </div>
          </>
        }
      >
        <div className="fgrid">
          <Field full label="NOMBRE">
            <input className="input" autoFocus placeholder="ej. Lentejas con verdura" value={name} onChange={(e) => setName(e.target.value)} />
          </Field>
          <Field full label="TIPOS DE COMIDA">
            <div className="recipe-filters" style={{ marginTop: 4 }}>
              {['Desayuno', 'Comida', 'Merienda', 'Cena'].map(k => (
                <button key={k} className={'chip' + (kinds.includes(k) ? ' active' : '')} onClick={() => toggleKind(k)} type="button">{k}</button>
              ))}
            </div>
          </Field>
        </div>

        <div className="row-between">
          <div className="t-mono-label dim">INGREDIENTES</div>
          <button className="btn btn--outline btn--sm" onClick={addIng}>+ Añadir</button>
        </div>

        <div style={{ border: '1px solid var(--frame)', borderRadius: 12, padding: '4px 14px' }}>
          {ings.map((row, i) => (
            <div className="ing-row" key={i}>
              <select className="select" value={row.name} onChange={(e) => handleIngName(i, e.target.value)}>
                <option value="">— Selecciona —</option>
                {sortedIngs.map(ing => <option key={ing.id} value={ing.name}>{ing.name}</option>)}
                <option value="__new__">+ Nuevo artículo…</option>
              </select>
              <input className="input" placeholder="Qty" value={row.qty} onChange={(e) => updIng(i, 'qty', e.target.value)} />
              <select className="select" value={row.unit} onChange={(e) => updIng(i, 'unit', e.target.value)}>
                <option>ud</option><option>g</option><option>kg</option><option>ml</option><option>L</option><option>cda</option>
              </select>
              <button className="x" onClick={() => delIng(i)}>×</button>
            </div>
          ))}
        </div>

        <Field full label="NOTAS / PASOS">
          <textarea className="input" rows="4" placeholder="Pasos opcionales…" style={{ width: '100%', resize: 'vertical' }} value={notes} onChange={(e) => setNotes(e.target.value)} />
        </Field>

        <Toggle checked={isPublic} onChange={setIsPublic} label="Hacer pública (visible para otros hogares)" />
      </ModalShell>
    </>
  );
}

// =========================================================
// EXPENSE — create or edit
// =========================================================
function ExpenseModal({ expense, onSave, onClose }) {
  const store = window.RC_STORE.useStore();
  const { categories, places, paymentAccounts } = store;
  const isNew = !expense?.id;

  const todayISO = new Date().toISOString().slice(0, 10);
  const [expDate, setExpDate] = useState(expense?.expense_date || expense?.date || todayISO);
  const [place, setPlace] = useState(expense?.place_name || expense?.place || '');
  const [catId, setCatId] = useState(() => {
    if (expense?.category_id) return String(expense.category_id);
    return categories?.[0]?.id ? String(categories[0].id) : '';
  });
  const [payment, setPayment] = useState(expense?.payment || 'Tarjeta');
  const [amount, setAmount] = useState(expense?.amount ? String(expense.amount) : '');
  const [details, setDetails] = useState(expense?.details || expense?.notes || '');
  const [busy, setBusy] = useState(false);
  const [forceCreate, setForceCreate] = useState(false);

  async function doSave() {
    const parsed = parseFloat(amount);
    if (!parsed || parsed <= 0) { window.showToast('Importe inválido', 'error'); return; }
    if (!expDate) { window.showToast('Fecha requerida', 'error'); return; }
    setBusy(true);
    try {
      const body = {
        expense_date: expDate,
        place_name: place.trim() || null,
        amount: parsed,
        payment,
        category_id: catId ? parseInt(catId, 10) : null,
        details,
        force: forceCreate,
      };
      if (isNew) {
        try {
          await window.api('/api/expenses', 'POST', body);
        } catch (e) {
          if (e.message && e.message.includes('Posible duplicado')) {
            setForceCreate(true);
            window.showToast('Posible duplicado detectado. Pulsa guardar de nuevo para confirmar.', 'info');
            setBusy(false);
            return;
          }
          throw e;
        }
      } else {
        await window.api(`/api/expenses/${expense.id}`, 'PUT', body);
      }
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  async function doDelete() {
    if (!expense?.id) return;
    setBusy(true);
    try {
      await window.api(`/api/expenses/${expense.id}`, 'DELETE');
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  return (
    <ModalShell
      kicker="GASTOS · ENTRADA"
      title={isNew ? 'Nuevo gasto' : `${place || '—'} · ${parseFloat(amount || 0).toFixed(2)}€`}
      onClose={onClose}
      foot={
        <>
          {!isNew && <button className="btn btn--ghost btn--sm" style={{ color: 'var(--tile-pink)' }} onClick={doDelete} disabled={busy}>Borrar gasto</button>}
          <div className="right">
            <button className="btn" onClick={onClose} disabled={busy}>Cancelar</button>
            <button className="btn btn--primary" onClick={doSave} disabled={busy}>
              {busy ? '…' : forceCreate ? 'Confirmar' : isNew ? 'Crear' : 'Guardar'}
            </button>
          </div>
        </>
      }
    >
      <div className="fgrid">
        <Field label="FECHA">
          <input className="input" type="date" value={expDate} onChange={(e) => setExpDate(e.target.value)} />
        </Field>
        <Field label="IMPORTE">
          <div style={{ position: 'relative' }}>
            <input className="input" type="number" step="0.01" min="0" value={amount} onChange={(e) => setAmount(e.target.value)} style={{ paddingRight: 28 }} />
            <span style={{ position: 'absolute', right: 12, top: '50%', transform: 'translateY(-50%)', color: 'var(--mint)', fontFamily: 'var(--f-mono)', fontSize: 13, fontWeight: 700 }}>€</span>
          </div>
        </Field>
        <Field full label="LUGAR">
          <input className="input" list="rc-places-live" value={place} onChange={(e) => setPlace(e.target.value)} placeholder="ej. Mercadona" />
          <datalist id="rc-places-live">
            {(places || []).map(p => <option key={p.id} value={p.name} />)}
          </datalist>
        </Field>
        <Field label="CATEGORÍA">
          <select className="select" value={catId} onChange={(e) => setCatId(e.target.value)}>
            <option value="">— Sin categoría —</option>
            {(categories || []).map(c => <option key={c.id} value={String(c.id)}>{c.icon} {c.name}</option>)}
          </select>
        </Field>
        <Field label="MÉTODO DE PAGO">
          <select className="select" value={payment} onChange={(e) => setPayment(e.target.value)}>
            {(paymentAccounts || []).length > 0
              ? (paymentAccounts || []).map(pa => <option key={pa.id} value={pa.name}>{pa.name}</option>)
              : [<option key="t" value="Tarjeta">Tarjeta</option>, <option key="e" value="Efectivo">Efectivo</option>]
            }
          </select>
        </Field>
        <Field full label="NOTAS (OPCIONAL)">
          <textarea className="input" rows="2" value={details} onChange={(e) => setDetails(e.target.value)} style={{ width: '100%', resize: 'vertical' }} />
        </Field>
      </div>
    </ModalShell>
  );
}

// =========================================================
// TICKET — verify OCR queue item
// =========================================================
function TicketViewModal({ ticketId, onSave, onClose }) {
  const [ticket, setTicket] = useState(null);
  const [loading, setLoading] = useState(true);
  const [editHeader, setEditHeader] = useState(false);
  const [hSuper, setHSuper] = useState('');
  const [hDate, setHDate] = useState('');
  const [hTotal, setHTotal] = useState('');
  const [itemEdits, setItemEdits] = useState({});
  const [busy, setBusy] = useState(false);

  const loadTicket = () => {
    setLoading(true);
    window.api(`/api/tickets/${ticketId}`)
      .then(t => {
        setTicket(t);
        setHSuper(t.supermarket || '');
        setHDate(t.date || '');
        setHTotal(String(t.total || ''));
      })
      .catch(e => window.showToast(e.message, 'error'))
      .finally(() => setLoading(false));
  };

  useEffect(() => { loadTicket(); }, [ticketId]);

  const items = ticket?.items || [];
  const total = ticket?.total ?? items.reduce((s, it) => s + Number(it.total_price || 0), 0);

  const updItem = (itemId, field, val) =>
    setItemEdits(e => ({ ...e, [itemId]: { ...(e[itemId] || {}), [field]: val } }));

  const getItemVal = (it, field) => {
    const ed = itemEdits[it.id];
    if (ed && field in ed) return ed[field];
    if (field === 'name') return it.product_name_normalized || it.name || it.raw_name || '';
    if (field === 'quantity') return it.quantity ?? 1;
    if (field === 'unit') return it.unit || 'ud';
    if (field === 'unit_price') return it.unit_price ?? 0;
    return '';
  };

  async function saveItem(it) {
    const ed = itemEdits[it.id];
    if (!ed) return;
    setBusy(true);
    try {
      await window.api(`/api/tickets/${ticketId}/items/${it.id}`, 'PATCH', {
        product_name_normalized: ed.name ?? getItemVal(it, 'name'),
        quantity: parseFloat(ed.quantity ?? getItemVal(it, 'quantity')) || 1,
        unit: ed.unit ?? getItemVal(it, 'unit'),
        unit_price: parseFloat(ed.unit_price ?? getItemVal(it, 'unit_price')) || 0,
      });
      setItemEdits(e => { const n = { ...e }; delete n[it.id]; return n; });
      loadTicket();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  async function saveHeader() {
    setBusy(true);
    try {
      await window.api(`/api/tickets/${ticketId}`, 'PUT', {
        supermarket: hSuper, date: hDate,
        total: parseFloat(hTotal) || undefined,
      });
      setEditHeader(false);
      loadTicket();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  async function doReparse() {
    setBusy(true);
    try {
      await window.api(`/api/tickets/${ticketId}/reparse`, 'POST');
      window.showToast('Recalculando…');
      if (onSave) await onSave();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  async function doDelete() {
    if (!window.confirm('¿Eliminar este ticket?')) return;
    setBusy(true);
    try {
      await window.api(`/api/tickets/${ticketId}`, 'DELETE');
      window.showToast('Ticket eliminado');
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  return (
    <ModalShell
      kicker={`TICKET #${ticketId}`}
      title={editHeader
        ? <input className="input" value={hSuper} onChange={e => setHSuper(e.target.value)} style={{ fontSize: 'inherit', fontWeight: 'inherit' }} />
        : (ticket?.supermarket || (loading ? 'Cargando…' : 'Ticket'))
      }
      size="xl"
      onClose={onClose}
      foot={
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, flex: 1 }}>
          <div style={{ display: 'flex', gap: 8 }}>
            <button className="btn btn--ghost btn--sm" onClick={doReparse} disabled={busy || loading}>🤖 Recalcular</button>
            {editHeader
              ? <button className="btn btn--ghost btn--sm" onClick={saveHeader} disabled={busy}>💾 Guardar cabecera</button>
              : <button className="btn btn--ghost btn--sm" onClick={() => setEditHeader(true)} disabled={busy}>✏️ Editar</button>
            }
            <button className="btn btn--ghost btn--sm" style={{ color: 'var(--tile-pink)' }} onClick={doDelete} disabled={busy}>🗑️ Eliminar</button>
          </div>
          <div style={{ marginLeft: 'auto', display: 'flex', gap: 12, alignItems: 'baseline' }}>
            {editHeader ? (
              <>
                <input className="input" style={{ width: 110 }} type="date" value={hDate} onChange={e => setHDate(e.target.value)} />
                <input className="input" style={{ width: 80 }} type="number" step="0.01" value={hTotal} onChange={e => setHTotal(e.target.value)} placeholder="Total" />
              </>
            ) : (
              <>
                <span className="t-mono-micro dim">TOTAL</span>
                <span className="t-display-3" style={{ fontSize: 28, color: 'var(--mint)' }}>{(total || 0).toFixed(2)}€</span>
                {ticket?.date && <span className="t-mono-micro dim">{ticket.date}</span>}
              </>
            )}
          </div>
        </div>
      }
    >
      {loading && <div className="t-body dim" style={{ padding: 24, textAlign: 'center' }}>Cargando…</div>}
      {!loading && ticket && (
        <div style={{ border: '1px solid var(--frame)', borderRadius: 12, padding: '4px 14px', maxHeight: 420, overflowY: 'auto' }}>
          <div className="tk-row" style={{ borderBottom: '1px solid var(--frame)' }}>
            <span className="t-mono-micro dim">PRODUCTO</span>
            <span className="t-mono-micro dim" style={{ textAlign: 'center' }}>QTY</span>
            <span className="t-mono-micro dim" style={{ textAlign: 'center' }}>UNIDAD</span>
            <span className="t-mono-micro dim" style={{ textAlign: 'right' }}>€/ud</span>
            <span></span>
          </div>
          {items.map((it) => {
            const dirty = !!itemEdits[it.id];
            return (
              <div className="tk-row" key={it.id ?? it.raw_name}>
                <div className="nm">
                  <input value={getItemVal(it, 'name')} onChange={e => updItem(it.id, 'name', e.target.value)} />
                </div>
                <div className="num">
                  <input type="number" step="0.01" value={getItemVal(it, 'quantity')} onChange={e => updItem(it.id, 'quantity', e.target.value)} />
                </div>
                <div className="num">
                  <input value={getItemVal(it, 'unit')} onChange={e => updItem(it.id, 'unit', e.target.value)} />
                </div>
                <div className="pr">
                  <input type="number" step="0.01" value={getItemVal(it, 'unit_price')} onChange={e => updItem(it.id, 'unit_price', e.target.value)} style={{ width: '100%', textAlign: 'right' }} />
                </div>
                <span>
                  {dirty && <button className="btn btn--ghost btn--sm" style={{ padding: '2px 6px' }} onClick={() => saveItem(it)} disabled={busy}>✓</button>}
                </span>
              </div>
            );
          })}
          {items.length === 0 && <div style={{ padding: '12px 0', color: 'var(--text-meta)', fontSize: 13, textAlign: 'center' }}>Sin ítems.</div>}
        </div>
      )}
    </ModalShell>
  );
}

function TicketModal({ queueItem, ticketId, onSave, onClose }) {
  if (ticketId) return <TicketViewModal ticketId={ticketId} onSave={onSave} onClose={onClose} />;

  const { categories, paymentAccounts } = window.RC_STORE.useStore();
  const f0 = window.RC_ticketFlat(queueItem?.ticket_data);
  const [supermarket, setSupermarket] = useState(f0.supermarket);
  const [ticketDate, setTicketDate] = useState(f0.date || new Date().toISOString().slice(0, 10));
  const [catId, setCatId] = useState(() => categories?.[0]?.id ? String(categories[0].id) : '');
  const [payment, setPayment] = useState('Tarjeta');
  const [items, setItems] = useState(f0.items);
  const [busy, setBusy] = useState(false);

  const upd = (i, k, v) => setItems(l => l.map((row, j) => j === i ? { ...row, [k]: v } : row));
  const del = (i) => setItems(l => l.filter((_, j) => j !== i));
  const addItem = () => setItems(l => [...l, { product_name: '', quantity: 1, unit: 'ud', unit_price: null, total_price: null }]);

  const sum = items.reduce((s, it) => s + (Number(it.unit_price || 0) * Number(it.quantity || 0)), 0);

  const doConfirm = async () => {
    if (!queueItem?.id) return;
    setBusy(true);
    try {
      await window.api(`/api/tickets/queue/${queueItem.id}/confirm`, 'POST', {
        ticket_data: window.RC_ticketBuild(queueItem?.ticket_data, { supermarket, date: ticketDate, total: sum, items }),
        expense_category_id: catId ? parseInt(catId, 10) : null,
        payment,
      });
      window.showToast('Ticket confirmado y gasto creado');
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  };

  const doDiscard = async () => {
    if (!queueItem?.id) return;
    setBusy(true);
    try {
      await window.api(`/api/tickets/queue/${queueItem.id}`, 'DELETE');
      window.showToast('Ticket descartado');
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  };

  return (
    <ModalShell
      kicker={`OCR · COLA #${queueItem?.id || '—'}`}
      title={supermarket || 'Verificar ticket'}
      size="xl"
      onClose={onClose}
      foot={
        <>
          <div className="row" style={{ gap: 12, alignItems: 'baseline' }}>
            <span className="t-mono-micro dim">TOTAL VERIFICADO</span>
            <span className="t-display-3" style={{ fontSize: 28, color: 'var(--mint)' }}>{sum.toFixed(2)}€</span>
            {f0.total != null && <span className="t-mono-micro dim">/ {f0.total.toFixed(2)}€ EXTRAÍDOS</span>}
          </div>
          <div className="right">
            <button className="btn btn--ghost btn--sm" style={{ color: 'var(--tile-pink)' }} onClick={doDiscard} disabled={busy}>Descartar</button>
            <button className="btn btn--primary" onClick={doConfirm} disabled={busy}>{busy ? '…' : 'Verificar y crear gasto'}</button>
          </div>
        </>
      }
    >
      <div style={{ display: 'grid', gridTemplateColumns: '220px 1fr', gap: 20 }}>
        <div>
          <div className="tk-thumb">
            <div className="imgph" data-tone="mint" data-label="FOTO" style={{ width: '100%', height: '100%' }}></div>
          </div>
          <div className="t-mono-micro dim" style={{ marginTop: 10 }}>{items.length} ÍTEMS DETECTADOS</div>
        </div>

        <div className="stack-3">
          <div className="fgrid">
            <Field label="SUPERMERCADO">
              <input className="input" value={supermarket} onChange={e => setSupermarket(e.target.value)} />
            </Field>
            <Field label="FECHA TICKET">
              <input className="input" type="date" value={ticketDate} onChange={e => setTicketDate(e.target.value)} />
            </Field>
            <Field label="CATEGORÍA GASTO">
              <select className="select" value={catId} onChange={e => setCatId(e.target.value)}>
                <option value="">— Sin categoría —</option>
                {categories.map(c => <option key={c.id} value={String(c.id)}>{c.icon} {c.name}</option>)}
              </select>
            </Field>
            <Field label="MÉTODO DE PAGO">
              <select className="select" value={payment} onChange={e => setPayment(e.target.value)}>
                {(paymentAccounts || []).length > 0
                  ? (paymentAccounts || []).map(pa => <option key={pa.id} value={pa.name}>{pa.name}</option>)
                  : [<option key="t" value="Tarjeta">Tarjeta</option>, <option key="e" value="Efectivo">Efectivo</option>]
                }
              </select>
            </Field>
          </div>

          <div className="row-between">
            <div className="t-mono-label dim">ÍTEMS DETECTADOS</div>
            <button className="btn btn--outline btn--sm" onClick={addItem}>+ Añadir ítem</button>
          </div>

          <div style={{ border: '1px solid var(--frame)', borderRadius: 12, padding: '4px 14px', maxHeight: 280, overflowY: 'auto' }}>
            <div className="tk-row" style={{ borderBottom: '1px solid var(--frame)' }}>
              <span className="t-mono-micro dim">PRODUCTO</span>
              <span className="t-mono-micro dim" style={{ textAlign: 'center' }}>QTY</span>
              <span className="t-mono-micro dim" style={{ textAlign: 'center' }}>UNIDAD</span>
              <span className="t-mono-micro dim" style={{ textAlign: 'right' }}>PRECIO</span>
              <span></span>
            </div>
            {items.map((it, i) => (
              <div className="tk-row" key={i}>
                <div className="nm"><input value={it.product_name || ''} onChange={e => upd(i, 'product_name', e.target.value)} /></div>
                <div className="num"><input type="number" step="0.01" value={it.quantity ?? 0} onChange={e => upd(i, 'quantity', parseFloat(e.target.value) || 0)} /></div>
                <div className="num"><input value={it.unit || 'ud'} onChange={e => upd(i, 'unit', e.target.value)} /></div>
                <div className="pr">{(Number(it.unit_price || 0) * Number(it.quantity || 0)).toFixed(2)}€</div>
                <button className="x" onClick={() => del(i)}>×</button>
              </div>
            ))}
            {items.length === 0 && <div style={{ padding: '12px 0', color: 'var(--text-meta)', fontSize: 13, textAlign: 'center' }}>Sin ítems detectados.</div>}
          </div>
        </div>
      </div>
    </ModalShell>
  );
}

// =========================================================
// ARTICLE — view / edit inventory article
// =========================================================
function ArticleModal({ article, onSave, onClose }) {
  const isNew = !article?.id;
  const [name, setName] = useState(article?.name || '');
  const [supermarket, setSupermarket] = useState(article?.supermarket || 'Mercadona');
  const [unit, setUnit] = useState(article?.unit || 'unidad');
  const [envaseQty, setEnvaseQty] = useState(article?.envase_qty ?? 1);
  const [stock, setStock] = useState(article?.stock ?? 0);
  const [minStock, setMinStock] = useState(article?.min_stock ?? 0);
  const [manualShop, setManualShop] = useState(!!article?.manual_shop);
  const [variableWeight, setVariableWeight] = useState(!!article?.variable_weight);
  const [obs, setObs] = useState(article?.obs || '');
  const [busy, setBusy] = useState(false);

  // Vínculo a producto canónico (precios) + convergencia de variantes
  const [canonicalName, setCanonicalName] = useState(article?.canonical_name || '');
  const [canonCandidates, setCanonCandidates] = useState([]);
  const [showCanon, setShowCanon] = useState(false);
  const [variantsChecked, setVariantsChecked] = useState(() => new Set());
  const canonDebounce = useRef(null);
  const hadCanonical = !!article?.canonical_name;

  async function fetchCanon(val) {
    const q = (val || '').trim();
    if (q.length < 2) { setCanonCandidates([]); return; }
    try {
      const data = await window.api(`/api/ingredients/canonical-suggest?q=${encodeURIComponent(q)}`);
      setCanonCandidates(data || []);
    } catch (_) { /* silencioso */ }
  }

  // Cargar candidatos al abrir (para mostrar variantes coincidentes)
  useEffect(() => {
    fetchCanon(name);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  function onCanonChange(val) {
    setCanonicalName(val);
    setShowCanon(true);
    clearTimeout(canonDebounce.current);
    canonDebounce.current = setTimeout(() => fetchCanon(val), 250);
  }

  function toggleVariant(v) {
    setVariantsChecked(prev => {
      const next = new Set(prev);
      if (next.has(v)) next.delete(v); else next.add(v);
      return next;
    });
  }

  // Autocompletar nombre (solo en creación, pool de otros hogares)
  const [suggestions, setSuggestions] = useState([]);
  const [showSuggest, setShowSuggest] = useState(false);
  const [dropPos, setDropPos] = useState({ top: 0, left: 0, width: 0 });
  const suggestDebounce = useRef(null);
  const nameInputRef = useRef(null);

  async function fetchSuggestions(val) {
    if (!isNew || val.trim().length < 2) { setSuggestions([]); setShowSuggest(false); return; }
    try {
      const data = await window.api(`/api/ingredients/suggest?q=${encodeURIComponent(val.trim())}`);
      setSuggestions(data || []);
      if ((data || []).length > 0 && nameInputRef.current) {
        const r = nameInputRef.current.getBoundingClientRect();
        setDropPos({ top: r.bottom + window.scrollY, left: r.left + window.scrollX, width: r.width });
        setShowSuggest(true);
      } else {
        setShowSuggest(false);
      }
    } catch (_) { /* silencioso */ }
  }

  function onNameChange(val) {
    setName(val);
    clearTimeout(suggestDebounce.current);
    suggestDebounce.current = setTimeout(() => { fetchSuggestions(val); fetchCanonForName(val); }, 250);
  }

  // Candidatos de canónico a partir del NOMBRE; auto-rellena si hay match de confianza
  async function fetchCanonForName(val) {
    const q = (val || '').trim();
    if (q.length < 2) return;
    try {
      const data = await window.api(`/api/ingredients/canonical-suggest?q=${encodeURIComponent(q)}`);
      setCanonCandidates(data || []);
      if (data && data[0] && data[0].score >= 1) {
        setCanonicalName(prev => prev.trim() ? prev : data[0].canonical);
      }
    } catch (_) { /* silencioso */ }
  }

  function applySuggestion(s) {
    setName(s.name);
    if (s.unit) setUnit(s.unit);
    if (s.envase_qty) setEnvaseQty(s.envase_qty);
    setSuggestions([]);
    setShowSuggest(false);
  }

  const low = stock < minStock;

  const doSave = async () => {
    if (!name.trim()) { window.showToast('Nombre requerido', 'error'); return; }
    setBusy(true);
    try {
      const cn = canonicalName.trim();
      // None → auto-vincular; "" → limpiar vínculo previo; valor → override manual
      const canonical_name = cn ? cn : (hadCanonical ? '' : null);
      const link_variants = cn && variantsChecked.size ? Array.from(variantsChecked) : undefined;
      const body = { name: name.trim(), supermarket, unit, envase_qty: parseFloat(envaseQty) || 1, stock: parseFloat(stock) || 0, min_stock: parseFloat(minStock) || 0, obs, variable_weight: variableWeight ? 1 : 0, canonical_name, link_variants };
      if (isNew) {
        await window.api('/api/ingredients', 'POST', body);
      } else {
        await window.api(`/api/ingredients/${article.id}`, 'PUT', body);
      }
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  };

  const doDelete = async () => {
    if (!article?.id) return;
    setBusy(true);
    try {
      await window.api(`/api/ingredients/${article.id}`, 'DELETE');
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  };

  return (
    <ModalShell
      kicker={`INVENTARIO · ${supermarket.toUpperCase()}`}
      title={isNew ? 'Nuevo artículo' : name}
      size="wide"
      onClose={onClose}
      foot={
        <>
          {!isNew && <button className="btn btn--ghost btn--sm" style={{ color: 'var(--tile-pink)' }} onClick={doDelete} disabled={busy}>Borrar</button>}
          <div className="right">
            <button className="btn" onClick={onClose} disabled={busy}>Cancelar</button>
            <button className="btn btn--primary" onClick={doSave} disabled={busy}>{busy ? '…' : isNew ? 'Crear' : 'Guardar'}</button>
          </div>
        </>
      }
    >
      {isNew ? null : (
        <div className="fgrid" style={{ marginBottom: 8 }}>
          <div className="card" style={{ padding: 16, borderColor: low ? 'var(--tile-pink)' : 'var(--mint)' }}>
            <div className="t-mono-label" style={{ color: low ? 'var(--tile-pink)' : 'var(--mint)' }}>{low ? 'BAJO MÍNIMO' : 'STOCK OK'}</div>
            <div className="t-display-3" style={{ marginTop: 6, fontSize: 36 }}>
              {stock} <span style={{ fontSize: 14, color: 'var(--text-meta)' }}>/ {minStock} mín</span>
            </div>
            <div className="row" style={{ marginTop: 12, gap: 6 }}>
              <button className="btn btn--ghost btn--sm" onClick={() => setStock(s => Math.max(0, +(s - 1).toFixed(2)))}>− 1</button>
              <button className="btn btn--ghost btn--sm" onClick={() => setStock(s => +(s + 1).toFixed(2))}>+ 1</button>
              <button className="btn btn--ghost btn--sm" onClick={() => setStock(s => +(s + 0.5).toFixed(2))}>+ 0,5</button>
            </div>
          </div>
          <div className="card" style={{ padding: 16 }}>
            <div className="t-mono-label dim">COMPRA MANUAL</div>
            <div className="t-body" style={{ marginTop: 8, color: 'var(--text-muted)' }}>{manualShop ? 'Sí' : 'No'}</div>
          </div>
        </div>
      )}

      <div className="fgrid">
        <Field full={isNew} label="NOMBRE">
          <div style={{ position: 'relative' }}>
            <input
              ref={nameInputRef}
              className="input"
              value={name}
              onChange={e => onNameChange(e.target.value)}
              onBlur={() => setTimeout(() => setShowSuggest(false), 150)}
              onFocus={() => {
                if (suggestions.length > 0 && nameInputRef.current) {
                  const r = nameInputRef.current.getBoundingClientRect();
                  setDropPos({ top: r.bottom + window.scrollY, left: r.left + window.scrollX, width: r.width });
                  setShowSuggest(true);
                }
              }}
              placeholder="ej. Aceite oliva"
              autoFocus={isNew}
              autoComplete="off"
            />
            {showSuggest && suggestions.length > 0 && (
              <div style={{
                position: 'fixed', top: dropPos.top, left: dropPos.left, width: dropPos.width,
                background: 'var(--slate)', border: '1px solid var(--frame)',
                borderRadius: 6, zIndex: 9999, maxHeight: 200, overflowY: 'auto',
                boxShadow: '0 6px 20px rgba(0,0,0,.5)',
              }}>
                {suggestions.map((s, i) => (
                  React.createElement('button', {
                    key: i, type: 'button',
                    style: { display: 'flex', width: '100%', padding: '8px 12px', background: 'none', border: 0, borderBottom: '1px solid var(--frame)', cursor: 'pointer', textAlign: 'left', gap: 8, alignItems: 'center', color: 'var(--text)' },
                    onMouseDown: (e) => { e.preventDefault(); applySuggestion(s); },
                  },
                    React.createElement('span', { style: { flex: 1, fontSize: 13 } }, s.name),
                    React.createElement('span', { style: { fontSize: 11, opacity: 0.6, fontFamily: 'var(--f-mono)' } }, s.unit || '')
                  )
                ))}
              </div>
            )}
          </div>
        </Field>
        <Field label="SUPERMERCADO">
          <input className="input" value={supermarket} onChange={e => setSupermarket(e.target.value)} />
        </Field>
        <Field label="UNIDAD">
          <input className="input" value={unit} onChange={e => setUnit(e.target.value)} />
        </Field>
        <Field label="ENVASE (CANT.)">
          <input className="input" type="number" step="0.01" value={envaseQty} onChange={e => setEnvaseQty(e.target.value)} />
        </Field>
        {isNew && (
          <Field label="STOCK INICIAL">
            <input className="input" type="number" step="0.01" value={stock} onChange={e => setStock(e.target.value)} />
          </Field>
        )}
        <Field label="MÍNIMO">
          <input className="input" type="number" step="0.01" value={minStock} onChange={e => setMinStock(e.target.value)} />
        </Field>
        <Field full label="OBSERVACIONES">
          <input className="input" value={obs} onChange={e => setObs(e.target.value)} placeholder="opcional" />
        </Field>
        <Field full label="PRODUCTO (PRECIOS)">
          <div style={{ position: 'relative' }}>
            <input
              className="input"
              value={canonicalName}
              onChange={e => onCanonChange(e.target.value)}
              onFocus={() => setShowCanon(true)}
              onBlur={() => setTimeout(() => setShowCanon(false), 150)}
              placeholder="se vincula solo · escribe para cambiarlo"
              autoComplete="off"
            />
            {showCanon && canonCandidates.length > 0 && (
              <div style={{
                position: 'absolute', top: '100%', left: 0, right: 0, marginTop: 2,
                background: 'var(--slate)', border: '1px solid var(--frame)',
                borderRadius: 6, zIndex: 9999, maxHeight: 200, overflowY: 'auto',
                boxShadow: '0 6px 20px rgba(0,0,0,.5)',
              }}>
                {canonCandidates.map((c, i) => (
                  React.createElement('button', {
                    key: i, type: 'button',
                    style: { display: 'flex', width: '100%', padding: '8px 12px', background: 'none', border: 0, borderBottom: '1px solid var(--frame)', cursor: 'pointer', textAlign: 'left', gap: 8, alignItems: 'center', color: 'var(--text)' },
                    onMouseDown: (e) => { e.preventDefault(); setCanonicalName(c.canonical); setShowCanon(false); },
                  },
                    React.createElement('span', { style: { flex: 1, fontSize: 13 } }, c.canonical),
                    React.createElement('span', { style: { fontSize: 11, opacity: 0.6, fontFamily: 'var(--f-mono)' } }, (c.freq ? c.freq + '×' : ''))
                  )
                ))}
              </div>
            )}
          </div>
        </Field>
        {canonicalName.trim() && canonCandidates.filter(c => c.canonical.toLowerCase() !== canonicalName.trim().toLowerCase()).length > 0 && (
          <Field full label="MISMAS VARIANTES EN TICKETS (UNIFICAR PRECIOS)">
            <div className="row" style={{ flexWrap: 'wrap', gap: 8 }}>
              {canonCandidates.filter(c => c.canonical.toLowerCase() !== canonicalName.trim().toLowerCase()).map((c, i) => (
                <label key={i} className="row" style={{ gap: 6, alignItems: 'center', fontSize: 12, cursor: 'pointer', padding: '4px 8px', border: '1px solid var(--frame)', borderRadius: 8 }}>
                  <input type="checkbox" checked={variantsChecked.has(c.canonical)} onChange={() => toggleVariant(c.canonical)} />
                  <span>{c.canonical}</span>
                </label>
              ))}
            </div>
          </Field>
        )}
      </div>

      <div className="row" style={{ gap: 20, flexWrap: 'wrap', padding: 14, border: '1px solid var(--frame)', borderRadius: 12 }}>
        <Toggle checked={manualShop} onChange={setManualShop} label="Marcar siempre para compra" />
        <Toggle checked={variableWeight} onChange={setVariableWeight} label="⚖️ Peso variable (el bot pedirá gramos tras cada ticket)" />
      </div>
    </ModalShell>
  );
}

// =========================================================
// ADHOC — quick single shopping item
// =========================================================
function AdhocModal({ onClose }) {
  return (
    <ModalShell
      kicker="COMPRA · ARTÍCULO ÚNICO"
      title="Añadir artículo"
      onClose={onClose}
      foot={
        <>
          <span className="t-mono-micro dim">SOLO PARA ESTA COMPRA · NO AFECTA INVENTARIO</span>
          <div className="right">
            <button className="btn" onClick={onClose}>Cancelar</button>
            <button className="btn btn--primary" onClick={onClose}>Añadir a lista</button>
          </div>
        </>
      }
    >
      <div className="fgrid">
        <Field full label="ARTÍCULO">
          <input className="input" autoFocus placeholder="ej. Tarta de cumpleaños" />
        </Field>
        <Field label="SUPERMERCADO">
          <select className="select" defaultValue="Mercadona">
            <option>Mercadona</option><option>Lidl</option><option>Carrefour</option><option>Otro</option>
          </select>
        </Field>
        <Field label="CANTIDAD">
          <input className="input" type="number" defaultValue={1} />
        </Field>
        <Field full label="NOTAS">
          <input className="input" placeholder="opcional" />
        </Field>
      </div>
    </ModalShell>
  );
}

// =========================================================
// UPLOAD TICKET
// =========================================================
function UploadTicketModal({ onSave, onClose }) {
  const [file, setFile] = useState(null);
  const [busy, setBusy] = useState(false);
  const fileRef = React.useRef(null);

  const onDrop = (e) => {
    e.preventDefault();
    const f = e.dataTransfer?.files?.[0];
    if (f) setFile(f);
  };

  const doUpload = async () => {
    if (!file) { window.showToast('Selecciona una imagen primero', 'error'); return; }
    setBusy(true);
    try {
      const form = new FormData();
      form.append('file', file);
      const res = await fetch('/api/tickets/upload', {
        method: 'POST',
        credentials: 'include',
        body: form,
      });
      if (res.status === 401) { window.location.reload(); return; }
      if (!res.ok) {
        let msg = `Error ${res.status}`;
        try { const d = await res.json(); msg = d.detail || msg; } catch {}
        throw new Error(msg);
      }
      window.showToast('Ticket en cola · recibirás una notificación cuando esté listo');
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  };

  return (
    <ModalShell
      kicker="TICKETS · SUBIR FOTO"
      title="Subir ticket"
      onClose={onClose}
      foot={
        <>
          <span className="t-mono-micro dim">JPEG · PNG · WEBP · MÁX 8 MB</span>
          <div className="right">
            <button className="btn" onClick={onClose} disabled={busy}>Cancelar</button>
            <button className="btn btn--primary" onClick={doUpload} disabled={busy || !file}>
              {busy ? '…' : 'Subir y procesar'}
            </button>
          </div>
        </>
      }
    >
      <div
        onDrop={onDrop}
        onDragOver={e => e.preventDefault()}
        onClick={() => fileRef.current?.click()}
        style={{
          border: `2px dashed ${file ? 'var(--mint)' : 'var(--frame)'}`,
          borderRadius: 16,
          padding: '48px 24px',
          textAlign: 'center',
          background: 'var(--slate-2)',
          cursor: 'pointer',
        }}
      >
        <input
          ref={fileRef}
          type="file"
          accept="image/jpeg,image/png,image/webp"
          style={{ display: 'none' }}
          onChange={e => setFile(e.target.files?.[0] || null)}
        />
        {file ? (
          <>
            <div className="t-display-3" style={{ fontSize: 24, color: 'var(--mint)' }}>{file.name}</div>
            <div className="t-body dim" style={{ marginTop: 8 }}>{(file.size / 1024 / 1024).toFixed(2)} MB · listo para subir</div>
          </>
        ) : (
          <>
            <div className="t-display-3" style={{ fontSize: 28 }}>Arrastra una foto aquí</div>
            <div className="t-body dim" style={{ marginTop: 8 }}>O pulsa para seleccionar</div>
            <button className="btn btn--outline" style={{ marginTop: 16 }} onClick={e => { e.stopPropagation(); fileRef.current?.click(); }}>Seleccionar archivo</button>
          </>
        )}
      </div>

      <div className="card" style={{ padding: 16 }}>
        <div className="t-mono-label dim">QUÉ PASA DESPUÉS</div>
        <ol style={{ margin: '12px 0 0 18px', padding: 0, fontFamily: 'var(--f-sans)', fontSize: 14, lineHeight: 1.6, color: 'var(--text-muted)' }}>
          <li>Validación: tipo, tamaño y eliminación de metadatos EXIF</li>
          <li>Cola FIFO · procesamiento con Gemini Vision (&lt; 1 min)</li>
          <li>Normalización de nombres y aliases</li>
          <li>Notificación cuando esté listo para verificar</li>
        </ol>
      </div>
    </ModalShell>
  );
}
// =========================================================
// TEMPLATE SLOT — assign recipe to a template slot
// =========================================================
function TemplateSlotModal({ dayIdx, mealType, dayLabel, slotLabel, onSave, onClose }) {
  const { recipes } = window.RC_STORE.useStore();
  const [recipeId, setRecipeId] = useState('');
  const [raciones, setRaciones] = useState(2);
  const [busy, setBusy] = useState(false);

  async function doSave() {
    if (!recipeId) { window.showToast('Elige una receta', 'error'); return; }
    setBusy(true);
    try {
      await window.api(`/api/template/${dayIdx}/${mealType}/recipes`, 'POST', {
        recipe_id: parseInt(recipeId, 10), raciones,
      });
      window.showToast('Plantilla actualizada');
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  return (
    <ModalShell
      kicker={`PLANTILLA · ${(dayLabel || '').toUpperCase()} · ${(slotLabel || '').toUpperCase()}`}
      title="Asignar receta"
      onClose={onClose}
      foot={
        <div className="right">
          <button className="btn" onClick={onClose} disabled={busy}>Cancelar</button>
          <button className="btn btn--primary" onClick={doSave} disabled={busy}>{busy ? '…' : 'Guardar'}</button>
        </div>
      }
    >
      <div className="fgrid">
        <Field full label="RECETA">
          <select className="select" value={recipeId} onChange={e => setRecipeId(e.target.value)}>
            <option value="">— elegir receta —</option>
            {recipes.map(r => <option key={r.id} value={String(r.id)}>{r.name}</option>)}
          </select>
        </Field>
        <Field label="RACIONES">
          <input className="input" type="number" min="1" max="20" value={raciones} onChange={e => setRaciones(+e.target.value)} />
        </Field>
      </div>
    </ModalShell>
  );
}

// =========================================================
// CATEGORY — create/edit expense category
// =========================================================
function CategoryModal({ category, onSave, onClose }) {
  const { reload } = window.RC_STORE.useStore();
  const isNew = !category?.id;
  const [icon, setIcon] = useState(category?.icon || '📦');
  const [name, setCatName] = useState(category?.name || '');
  const [budget, setBudget] = useState(category?.monthly_limit ?? '');
  const [busy, setBusy] = useState(false);

  async function doSave() {
    if (!name.trim()) { window.showToast('Nombre requerido', 'error'); return; }
    setBusy(true);
    try {
      let catId = category?.id;
      if (isNew) {
        const created = await window.api('/api/expense-categories', 'POST', {
          name: name.trim(), icon, sort_order: 99,
        });
        catId = created?.id;
      } else {
        await window.api(`/api/expense-categories/${catId}`, 'PUT', {
          name: name.trim(), icon,
        });
      }
      if (catId && budget !== '') {
        await window.api(`/api/expense-budgets/${catId}`, 'PUT', {
          monthly_limit: parseFloat(budget) || 0,
        });
      }
      window.showToast(isNew ? 'Categoría creada' : 'Categoría actualizada');
      if (reload?.expenses) await reload.expenses();
      if (reload?.static) await reload.static();
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  async function doDelete() {
    if (!category?.id) return;
    if (!window.confirm('¿Eliminar esta categoría?')) return;
    setBusy(true);
    try {
      await window.api(`/api/expense-categories/${category.id}`, 'DELETE');
      window.showToast('Categoría eliminada');
      if (reload?.expenses) await reload.expenses();
      if (reload?.static) await reload.static();
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  return (
    <ModalShell
      kicker={isNew ? 'CATEGORÍAS · NUEVA' : 'CATEGORÍAS · EDITAR'}
      title={isNew ? 'Nueva categoría' : (category?.name || '')}
      onClose={onClose}
      foot={
        <>
          {!isNew && <button className="btn btn--ghost btn--sm" style={{ color: 'var(--tile-pink)' }} onClick={doDelete} disabled={busy}>Borrar</button>}
          <div className="right">
            <button className="btn" onClick={onClose} disabled={busy}>Cancelar</button>
            <button className="btn btn--primary" onClick={doSave} disabled={busy}>{busy ? '…' : isNew ? 'Crear' : 'Guardar'}</button>
          </div>
        </>
      }
    >
      <div className="fgrid">
        <Field label="ICONO / EMOJI">
          <input className="input" value={icon} onChange={e => setIcon(e.target.value)} placeholder="📦" style={{ fontSize: 22 }} />
        </Field>
        <Field full label="NOMBRE">
          <input className="input" autoFocus value={name} onChange={e => setCatName(e.target.value)} placeholder="ej. Supermercado" />
        </Field>
        <Field full label="PRESUPUESTO MENSUAL (€)">
          <input className="input" type="number" step="0.01" min="0" value={budget} onChange={e => setBudget(e.target.value)} placeholder="0.00" />
        </Field>
      </div>
    </ModalShell>
  );
}

// =========================================================
// PLACE — create/edit expense place
// =========================================================
function PlaceModal({ place, onSave, onClose }) {
  const { categories, paymentAccounts, reload } = window.RC_STORE.useStore();
  const isNew = !place?.id;
  const [name, setPlaceName] = useState(place?.name || '');
  const [catId, setCatId] = useState(place?.default_category_id ? String(place.default_category_id) : '');
  const [payment, setPayment] = useState(place?.default_payment || 'Tarjeta');
  const [busy, setBusy] = useState(false);

  async function doSave() {
    if (!name.trim()) { window.showToast('Nombre requerido', 'error'); return; }
    setBusy(true);
    try {
      const body = {
        name: name.trim(),
        default_category_id: catId ? parseInt(catId, 10) : null,
        default_payment: payment,
      };
      if (isNew) {
        await window.api('/api/expense-places', 'POST', body);
      } else {
        await window.api(`/api/expense-places/${place.id}`, 'PUT', body);
      }
      window.showToast(isNew ? 'Lugar creado' : 'Lugar actualizado');
      if (reload?.static) await reload.static();
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  async function doDelete() {
    if (!place?.id) return;
    if (!window.confirm('¿Eliminar este lugar?')) return;
    setBusy(true);
    try {
      await window.api(`/api/expense-places/${place.id}`, 'DELETE');
      window.showToast('Lugar eliminado');
      if (reload?.static) await reload.static();
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  return (
    <ModalShell
      kicker={isNew ? 'LUGARES · NUEVO' : 'LUGARES · EDITAR'}
      title={isNew ? 'Nuevo lugar' : (place?.name || '')}
      onClose={onClose}
      foot={
        <>
          {!isNew && <button className="btn btn--ghost btn--sm" style={{ color: 'var(--tile-pink)' }} onClick={doDelete} disabled={busy}>Borrar</button>}
          <div className="right">
            <button className="btn" onClick={onClose} disabled={busy}>Cancelar</button>
            <button className="btn btn--primary" onClick={doSave} disabled={busy}>{busy ? '…' : isNew ? 'Crear' : 'Guardar'}</button>
          </div>
        </>
      }
    >
      <div className="fgrid">
        <Field full label="NOMBRE">
          <input className="input" autoFocus value={name} onChange={e => setPlaceName(e.target.value)} placeholder="ej. Mercadona" />
        </Field>
        <Field full label="CATEGORÍA POR DEFECTO">
          <select className="select" value={catId} onChange={e => setCatId(e.target.value)}>
            <option value="">— ninguna —</option>
            {categories.map(c => <option key={c.id} value={String(c.id)}>{c.icon || ''} {c.name}</option>)}
          </select>
        </Field>
        <Field full label="PAGO POR DEFECTO">
          <select className="select" value={payment} onChange={e => setPayment(e.target.value)}>
            {(paymentAccounts || []).length > 0
              ? (paymentAccounts || []).map(pa => <option key={pa.id} value={pa.name}>{pa.name}</option>)
              : [<option key="t" value="Tarjeta">Tarjeta</option>, <option key="e" value="Efectivo">Efectivo</option>]
            }
          </select>
        </Field>
      </div>
    </ModalShell>
  );
}

// =========================================================
// PLANNING RECIPE DETAIL — ingredientes + acciones de planning
// =========================================================
function PlanningRecipeModal({ planRecipe, recipe, onSave, onClose }) {
  const [raciones, setRaciones] = useState(planRecipe.raciones || 1);
  const [busy, setBusy] = useState(false);
  const ings = recipe?.ingredients || [];

  async function doSave() {
    setBusy(true);
    try {
      await window.api(`/api/planning/recipes/${planRecipe.id}`, 'PATCH', { raciones });
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  async function toggleNotConsumed() {
    setBusy(true);
    try {
      await window.api(`/api/planning/recipes/${planRecipe.id}`, 'PATCH', { not_consumed: !planRecipe.notConsumed, raciones });
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  async function doDelete() {
    setBusy(true);
    try {
      await window.api(`/api/planning/recipes/${planRecipe.id}`, 'DELETE');
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  return (
    <ModalShell
      kicker="PLANNING"
      title={planRecipe.title}
      onClose={onClose}
      foot={
        <>
          <button className="btn btn--ghost btn--sm"
            style={{ color: 'var(--tile-pink)' }}
            onClick={doDelete} disabled={busy}>
            Eliminar
          </button>
          <div className="right">
            <button className="btn" onClick={toggleNotConsumed} disabled={busy}>
              {planRecipe.notConsumed ? 'Consumido' : 'No consumido'}
            </button>
            <button className="btn btn--primary" onClick={doSave} disabled={busy}>{busy ? '…' : 'Guardar'}</button>
          </div>
        </>
      }
    >
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
        <div className="t-mono-label" style={{ flex: 1 }}>RACIONES</div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <button className="btn btn--ghost btn--sm" style={{ padding: '2px 8px', fontSize: 16, lineHeight: 1 }}
            onClick={() => setRaciones(r => Math.max(1, r - 1))} disabled={busy}>−</button>
          <span className="t-mono-label" style={{ minWidth: 24, textAlign: 'center' }}>{raciones}</span>
          <button className="btn btn--ghost btn--sm" style={{ padding: '2px 8px', fontSize: 16, lineHeight: 1 }}
            onClick={() => setRaciones(r => r + 1)} disabled={busy}>+</button>
        </div>
      </div>
      <div className="t-mono-label" style={{ marginBottom: 10 }}>INGREDIENTES</div>
      {ings.length === 0 && <div className="t-body dim">Sin ingredientes registrados.</div>}
      {ings.map((i, ix) => (
        <div key={ix} className="lr" style={{ display: 'flex', justifyContent: 'space-between', padding: '7px 0', borderBottom: ix < ings.length - 1 ? '1px dashed var(--frame)' : 0 }}>
          <span>{i.name}</span>
          <span className="qq">{i.qty} {i.unit}</span>
        </div>
      ))}
    </ModalShell>
  );
}

// =========================================================
// CHANGE PASSWORD — cuenta
// =========================================================
function ChangePasswordModal({ onClose }) {
  const [cur, setCur] = useState('');
  const [pw, setPw]   = useState('');
  const [pw2, setPw2] = useState('');
  const [busy, setBusy] = useState(false);

  async function doSave() {
    if (pw.length < 8) { window.showToast('La nueva contraseña debe tener al menos 8 caracteres', 'error'); return; }
    if (pw !== pw2)    { window.showToast('Las contraseñas no coinciden', 'error'); return; }
    setBusy(true);
    try {
      await window.api('/api/auth/change-password', 'POST', { current_password: cur, new_password: pw });
      window.showToast('Contraseña actualizada');
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  return (
    <ModalShell
      kicker="CUENTA"
      title="Cambiar contraseña"
      onClose={onClose}
      foot={
        <div className="right">
          <button className="btn" onClick={onClose} disabled={busy}>Cancelar</button>
          <button className="btn btn--primary" onClick={doSave} disabled={busy}>{busy ? '…' : 'Guardar'}</button>
        </div>
      }
    >
      <div className="fgrid">
        <Field full label="CONTRASEÑA ACTUAL">
          <input className="input" type="password" autoComplete="current-password" value={cur} onChange={e => setCur(e.target.value)} />
        </Field>
        <Field full label="NUEVA CONTRASEÑA (MÍN. 8)">
          <input className="input" type="password" autoComplete="new-password" value={pw} onChange={e => setPw(e.target.value)} />
        </Field>
        <Field full label="REPETIR NUEVA CONTRASEÑA">
          <input className="input" type="password" autoComplete="new-password" value={pw2}
            onChange={e => setPw2(e.target.value)} onKeyDown={e => e.key === 'Enter' && doSave()} />
        </Field>
      </div>
    </ModalShell>
  );
}

function ModalHost() {
  const [modal, setModal] = useState(null);

  useEffect(() => {
    const onModal = (e) => setModal(e.detail);
    window.addEventListener('rc:modal', onModal);
    return () => window.removeEventListener('rc:modal', onModal);
  }, []);

  if (!modal || !modal.name) return null;
  const close = () => setModal(null);
  const p = modal.props || {};

  switch (modal.name) {
    case 'planning-recipe': return <PlanningRecipeModal {...p} onClose={close} />;
    case 'meal':           return <MealModal          {...p} onClose={close} />;
    case 'loose':          return <LooseModal         {...p} onClose={close} />;
    case 'recipe':         return <RecipeModal        {...p} onClose={close} />;
    case 'new-recipe':     return <NewRecipeModal     {...p} onClose={close} />;
    case 'expense':        return <ExpenseModal       {...p} onClose={close} />;
    case 'ticket':         return <TicketModal        {...p} onClose={close} />;
    case 'article':        return <ArticleModal       {...p} onClose={close} />;
    case 'adhoc':          return <AdhocModal                onClose={close} />;
    case 'upload-ticket':  return <UploadTicketModal        onClose={close} />;
    case 'template-slot':  return <TemplateSlotModal  {...p} onClose={close} />;
    case 'category':       return <CategoryModal      {...p} onClose={close} />;
    case 'place':          return <PlaceModal         {...p} onClose={close} />;
    case 'qty':            return <QtyModal           {...p} onClose={close} />;
    case 'public-recipes':    return <PublicRecipesModal    {...p} onClose={close} />;
    case 'payment-account':   return <PaymentAccountModal  {...p} onClose={close} />;
    case 'change-password':   return <ChangePasswordModal        onClose={close} />;
    default: return null;
  }
}

// =========================================================
// QTY — lightweight quantity prompt
// =========================================================
function QtyModal({ title, defaultValue, onConfirm, onClose }) {
  const [value, setValue] = useState(defaultValue ?? 1);

  const doConfirm = () => {
    if (onConfirm) onConfirm(Math.max(1, parseInt(value, 10) || 1));
    onClose();
  };

  return (
    <ModalShell
      kicker="CANTIDAD"
      title={title || '¿Cuántos?'}
      onClose={onClose}
      foot={
        <div className="right">
          <button className="btn" onClick={onClose}>Cancelar</button>
          <button className="btn btn--primary" onClick={doConfirm}>Confirmar</button>
        </div>
      }
    >
      <Field full label="CANTIDAD (ENVASES)">
        <input
          className="input"
          type="number"
          min="1"
          autoFocus
          value={value}
          onChange={e => setValue(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && doConfirm()}
        />
      </Field>
    </ModalShell>
  );
}

// =========================================================
// PAYMENT ACCOUNT — create or edit
// =========================================================
function PaymentAccountModal({ account, onSave, onClose }) {
  const isNew = !account?.id;
  const [name, setAccName] = useState(account?.name || '');
  const [busy, setBusy] = useState(false);

  async function doSave() {
    if (!name.trim()) { window.showToast('Nombre requerido', 'error'); return; }
    setBusy(true);
    try {
      if (isNew) {
        await window.api('/api/payment-accounts', 'POST', { name: name.trim() });
      } else {
        await window.api(`/api/payment-accounts/${account.id}`, 'PUT', { name: name.trim() });
      }
      window.showToast(isNew ? 'Cuenta creada' : 'Cuenta actualizada');
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  async function doDelete() {
    if (!account?.id) return;
    setBusy(true);
    try {
      await window.api(`/api/payment-accounts/${account.id}`, 'DELETE');
      window.showToast('Cuenta eliminada');
      if (onSave) await onSave();
      onClose();
    } catch (e) { window.showToast(e.message, 'error'); }
    finally { setBusy(false); }
  }

  return (
    <ModalShell
      kicker="GASTOS · CUENTAS DE PAGO"
      title={isNew ? 'Nueva cuenta' : account.name}
      onClose={onClose}
      foot={
        <>
          {!isNew && (
            <button className="btn btn--ghost btn--sm" style={{ color: 'var(--tile-pink)' }} onClick={doDelete} disabled={busy}>
              Borrar
            </button>
          )}
          <div className="right">
            <button className="btn" onClick={onClose} disabled={busy}>Cancelar</button>
            <button className="btn btn--primary" onClick={doSave} disabled={busy}>{busy ? '…' : isNew ? 'Crear' : 'Guardar'}</button>
          </div>
        </>
      }
    >
      <Field full label="NOMBRE">
        <input
          className="input"
          autoFocus
          value={name}
          onChange={e => setAccName(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && doSave()}
          placeholder="ej. Tarjeta común, Efectivo Patrick…"
        />
      </Field>
    </ModalShell>
  );
}

// =========================================================
// PUBLIC RECIPES EXPLORER
// =========================================================
function PublicRecipesModal({ onSave, onClose }) {
  const [q, setQ] = useState('');
  const [recipes, setRecipes] = useState([]);
  const [loading, setLoading] = useState(false);
  const [cloning, setCloning] = useState(null);

  async function search(query) {
    setLoading(true);
    try {
      const params = query.trim() ? `?q=${encodeURIComponent(query.trim())}` : '';
      const data = await window.api(`/api/recipes/public${params}`);
      setRecipes(data || []);
    } catch (e) {
      window.showToast(e.message, 'error');
    } finally {
      setLoading(false);
    }
  }

  useEffect(() => { search(''); }, []);

  const debounceRef = useRef(null);
  function onQueryChange(val) {
    setQ(val);
    clearTimeout(debounceRef.current);
    debounceRef.current = setTimeout(() => search(val), 300);
  }

  async function doClone(r) {
    setCloning(r.id);
    try {
      const created = await window.api(`/api/recipes/${r.id}/clone`, 'POST');
      window.showToast(`"${created.name}" añadida a tus recetas`);
      if (onSave) await onSave();
    } catch (e) {
      window.showToast(e.message, 'error');
    } finally {
      setCloning(null);
    }
  }

  return (
    <ModalShell
      kicker="RECETAS"
      title="Explorar públicas"
      size="wide"
      onClose={onClose}
      foot={<div className="right"><button className="btn" onClick={onClose}>Cerrar</button></div>}
    >
      <div style={{ marginBottom: 12 }}>
        <input
          className="input"
          placeholder="Buscar receta…"
          value={q}
          onChange={e => onQueryChange(e.target.value)}
          autoFocus
        />
      </div>

      {loading && <div className="t-mono-label dim" style={{ textAlign: 'center', padding: 24 }}>Cargando…</div>}

      {!loading && recipes.length === 0 && (
        <div className="t-mono-label dim" style={{ textAlign: 'center', padding: 24 }}>
          No hay recetas públicas{q ? ' con ese nombre' : ''}.
        </div>
      )}

      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {recipes.map(r => {
          const meal_types = Array.isArray(r.meal_types) ? r.meal_types : (r.meal_types ? JSON.parse(r.meal_types) : []);
          const isBusy = cloning === r.id;
          return (
            <div
              key={r.id}
              style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 12px', background: 'var(--surface-2)', borderRadius: 8 }}
            >
              <div style={{ flex: 1, minWidth: 0 }}>
                <div className="t-body" style={{ fontWeight: 600 }}>{r.name}</div>
                <div className="t-mono-label dim" style={{ marginTop: 2 }}>
                  {r.owner_name && <span style={{ marginRight: 8 }}>👤 {r.owner_name}</span>}
                  {meal_types.map(k => <span key={k} className="tag tag--outline" style={{ marginRight: 4 }}>{k}</span>)}
                </div>
                {r.notes && <div className="t-sans-sm dim" style={{ marginTop: 2 }}>{r.notes}</div>}
              </div>
              <button
                className="btn btn--primary btn--sm"
                disabled={isBusy}
                onClick={() => doClone(r)}
              >
                {isBusy ? '…' : '+ Añadir'}
              </button>
            </div>
          );
        })}
      </div>
    </ModalShell>
  );
}

window.RC_ModalHost = ModalHost;
// Convenience: any code can call window.rcOpen('expense', {...})
window.rcOpen = (name, props) => window.dispatchEvent(new CustomEvent('rc:modal', { detail: { name, props } }));
