// Watt Spot - Admin dashboard and all admin screens
// Phase 2: wired to Supabase via window.WS_DB (with localStorage fallback).

const { useState: useAs, useEffect: useAe, useMemo: useAm, useRef: useAr } = React;

// ==================================================================
//  Product image normalization
// ==================================================================
// Every product photo, whatever its source resolution/aspect ratio, gets
// letterboxed onto a fixed 1500x1500 white canvas before upload. This is
// what makes every ws-prod-card thumbnail the same shape in the catalog —
// without it, cards render at whatever aspect ratio the uploaded file
// happened to have (portrait phone photos, wide manufacturer banners,
// etc.), which is why new products looked inconsistent next to older ones.
const PRODUCT_IMAGE_SIZE = 1500;
const normalizeProductImage = (file) => new Promise((resolve, reject) => {
  const reader = new FileReader();
  reader.onerror = () => reject(new Error("Lecture du fichier impossible"));
  reader.onload = (e) => {
    const img = new Image();
    img.onerror = () => reject(new Error("Image invalide"));
    img.onload = () => {
      const size = PRODUCT_IMAGE_SIZE;
      const scale = Math.min(size / img.width, size / img.height);
      const w = Math.round(img.width * scale);
      const h = Math.round(img.height * scale);
      const canvas = document.createElement('canvas');
      canvas.width = size;
      canvas.height = size;
      const ctx = canvas.getContext('2d');
      // White background so letterboxed edges match the card thumbnail's
      // resting background instead of showing as transparent/black bars.
      ctx.fillStyle = '#ffffff';
      ctx.fillRect(0, 0, size, size);
      ctx.drawImage(img, Math.round((size - w) / 2), Math.round((size - h) / 2), w, h);
      canvas.toBlob((blob) => {
        if (!blob) return reject(new Error("Conversion de l'image impossible"));
        const name = (file.name || 'image').replace(/\.[^.]+$/, '') + '.jpg';
        resolve(new File([blob], name, { type: 'image/jpeg' }));
      }, 'image/jpeg', 0.9);
    };
    img.src = e.target.result;
  };
  reader.readAsDataURL(file);
});

// ==================================================================
//  Pagination (shared)
// ==================================================================
const PAGE_SIZE = 20;
const Pagination = ({ page, total, pageSize = PAGE_SIZE, onPage }) => {
  const totalPages = Math.max(1, Math.ceil(total / pageSize));
  if (totalPages <= 1) {
    return (
      <div className="adm-pagination">
        <span className="ws-muted">Affichage 1–{total} sur {total}</span>
      </div>
    );
  }
  const from = (page - 1) * pageSize + 1;
  const to   = Math.min(total, page * pageSize);

  // Build page list with ellipses for long ranges
  const nums = [];
  const push = (n) => nums.push(n);
  if (totalPages <= 7) {
    for (let i = 1; i <= totalPages; i++) push(i);
  } else {
    push(1);
    if (page > 3) push('…');
    for (let i = Math.max(2, page - 1); i <= Math.min(totalPages - 1, page + 1); i++) push(i);
    if (page < totalPages - 2) push('…');
    push(totalPages);
  }

  return (
    <div className="adm-pagination">
      <span className="ws-muted">Affichage {from}–{to} sur {total}</span>
      <div className="adm-pages">
        <button disabled={page === 1} onClick={()=>onPage(page - 1)}><Icon name="chevL" size={12}/></button>
        {nums.map((n, i) => n === '…'
          ? <span key={'e' + i} style={{ padding: '0 6px', color: 'var(--muted)' }}>…</span>
          : <button key={n} className={n === page ? 'active' : ''} onClick={()=>onPage(n)}>{n}</button>
        )}
        <button disabled={page === totalPages} onClick={()=>onPage(page + 1)}><Icon name="chevR" size={12}/></button>
      </div>
    </div>
  );
};

// ==================================================================
//  Shared: tiny toast + confirm modal
// ==================================================================
const Toast = ({ msg, kind = 'ok', onClose }) => {
  useAe(() => { const t = setTimeout(onClose, 2800); return () => clearTimeout(t); }, []);
  return <div className={`ws-toast ws-toast-${kind}`}>{msg}</div>;
};
let pushToast = () => {};

const ToastHost = () => {
  const [toasts, setToasts] = useAs([]);
  useAe(() => {
    pushToast = (msg, kind = 'ok') => {
      const id = Math.random().toString(36).slice(2);
      setToasts(t => [...t, { id, msg, kind }]);
    };
    return () => { pushToast = () => {}; };
  }, []);
  return (
    <div className="ws-toast-host">
      {toasts.map(t => (
        <Toast key={t.id} msg={t.msg} kind={t.kind}
               onClose={() => setToasts(x => x.filter(y => y.id !== t.id))}/>
      ))}
    </div>
  );
};

const ConfirmModal = ({ title, message, onConfirm, onCancel, danger }) => (
  <div className="ws-modal-back" onClick={onCancel}>
    <div className="ws-modal" onClick={e => e.stopPropagation()}>
      <h3 style={{ margin: 0 }}>{title}</h3>
      <p className="ws-muted" style={{ marginTop: 8 }}>{message}</p>
      <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 18 }}>
        <button className="ws-btn ws-btn-ghost" onClick={onCancel}>Annuler</button>
        <button className={`ws-btn ${danger ? 'ws-btn-danger' : 'ws-btn-primary'}`} onClick={onConfirm}>
          {danger ? 'Supprimer' : 'Confirmer'}
        </button>
      </div>
    </div>
  </div>
);

// ==================================================================
//  Shared: stock status (3-state availability, replaces raw quantity)
// ==================================================================
const STOCK_STATUSES = {
  disponible:  { label: 'Disponible',       color: 'var(--ok)',     bg: 'rgba(21,159,106,0.12)', icon: 'check' },
  en_arrivage: { label: 'En arrivage',      color: 'var(--warn)',   bg: 'rgba(208,142,40,0.12)', icon: 'box'   },
  rupture:     { label: 'Rupture de stock', color: 'var(--danger)', bg: 'rgba(216,68,58,0.12)',  icon: 'warn'  },
};
const stockStatusInfo = (status) => STOCK_STATUSES[status] || STOCK_STATUSES.disponible;

const StockStatusPill = ({ status }) => {
  const s = stockStatusInfo(status);
  return (
    <span className="adm-stock-pill" style={{ color: s.color, background: s.bg }}>
      <Icon name={s.icon} size={11}/> {s.label}
    </span>
  );
};

// ==================================================================
//  Admin Shell
// ==================================================================
const AdminShell = ({ route, params, onNav, children, onClientView }) => {
  const sess = (window.WS_DB && window.WS_DB.session && window.WS_DB.session()) || null;
  const initials = sess
    ? ((sess.prenom || '?')[0] + (sess.nom || '')[0]).toUpperCase().slice(0, 2)
    : 'AD';
  const [sideOpen, setSideOpen] = useAs(false);

  // Auto-close the mobile drawer whenever the user navigates
  useAe(() => { setSideOpen(false); }, [route]);

  const nav = [
    { k: 'dashboard', l: 'Tableau de bord', i: 'dashboard' },
    { k: 'orders',    l: 'Commandes',       i: 'box' },
    { k: 'stock',     l: 'Stock',           i: 'cube' },
    { k: 'products',  l: 'Produits',        i: 'tag' },
    { k: 'cats',      l: 'Catégories',      i: 'grid' },
    { k: 'brands',    l: 'Marques',         i: 'tag' },
    { k: 'customers', l: 'Clients',         i: 'users' },
    { k: 'reports',   l: 'Rapports',        i: 'chart' },
  ];
  return (
    <div className={`adm-shell ${sideOpen ? 'side-open' : ''}`}>
      <div className="adm-side-overlay" onClick={() => setSideOpen(false)}/>
      <aside className="adm-sidebar">
        <div className="adm-logo">
          <svg width="28" height="28" viewBox="0 0 100 100"><path d="M52 8 L78 8 L60 38 L82 38 L30 92 L44 56 L22 56 Z" fill="var(--accent)"/></svg>
          <div><strong>Watt Spot</strong><span>Back-office</span></div>
          <button className="adm-side-close" onClick={() => setSideOpen(false)} aria-label="Fermer">
            <Icon name="close" size={18}/>
          </button>
        </div>
        <nav className="adm-nav">
          <div className="adm-nav-label">Général</div>
          {nav.map(n => (
            <button key={n.k} className={route==='admin-'+n.k?'active':''} onClick={()=>onNav('admin-'+n.k)}>
              <Icon name={n.i} size={16}/><span>{n.l}</span>
            </button>
          ))}
          <div className="adm-nav-label" style={{ marginTop: 20 }}>Système</div>
          <button onClick={async () => {
            if (!confirm('Se déconnecter de l\'espace administrateur ?')) return;
            try { await window.WS_DB?.logout(); } catch(e) {}
            onNav('home');
          }}><Icon name="logout" size={16}/><span>Déconnexion</span></button>
        </nav>
        <div className="adm-admin-card">
          <div className="ws-avatar">{initials}</div>
          <div>
            <strong>{sess ? `${sess.prenom || ''} ${sess.nom || ''}`.trim() || 'Admin' : 'Admin'}</strong>
            <span>{sess?.email || 'admin@wattspot.tn'}</span>
          </div>
        </div>
      </aside>
      <div className="adm-main">
        <header className="adm-topbar">
          <button className="adm-burger" onClick={() => setSideOpen(true)} aria-label="Menu">
            <Icon name="menu" size={20}/>
          </button>
          <AdminTopSearch onNav={onNav}/>
          <div className="adm-topbar-actions">
            <span className="ws-chip ws-chip-default adm-conn-chip" title={window.WS_DB?.isLive?.() ? 'Connecté à Supabase' : 'Données en localStorage'}>
              {window.WS_DB?.isLive?.() ? '● Supabase' : '● Démo'}
            </span>
            <AdminNotifBell onNav={onNav}/>
          </div>
        </header>
        <div className="adm-content">{children}</div>
        <ToastHost/>
      </div>
    </div>
  );
};

// ==================================================================
//  Admin Top Search — autocomplete across orders / products / clients
// ==================================================================
const AdminTopSearch = ({ onNav }) => {
  const [q, setQ] = useAs('');
  const [open, setOpen] = useAs(false);
  const [data, setData] = useAs({ orders: [], products: [], customers: [] });
  const wrapRef = useAr(null);

  useAe(() => {
    let alive = true;
    const load = () => Promise.all([
      window.WS_DB.listOrders().catch(() => []),
      window.WS_DB.listProducts({ activeOnly: false }).catch(() => []),
      window.WS_DB.listCustomers().catch(() => []),
    ]).then(([orders, products, customers]) => {
      if (alive) setData({ orders, products, customers });
    });
    load();
    const t = setInterval(load, 30000);
    return () => { alive = false; clearInterval(t); };
  }, []);

  useAe(() => {
    const h = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, []);

  const query = q.trim().toLowerCase();
  const matches = useAm(() => {
    if (query.length < 2) return { orders: [], products: [], customers: [] };
    const orders = data.orders.filter(o =>
      (o.numero || o.id || '').toString().toLowerCase().includes(query)
      || (o.client || '').toLowerCase().includes(query)
      || (o.email || '').toLowerCase().includes(query)
      || (o.tel || '').toLowerCase().includes(query)
    ).slice(0, 5);
    const products = data.products.filter(p =>
      (p.nom || '').toLowerCase().includes(query)
      || (p.ref || '').toLowerCase().includes(query)
      || (p.marque || '').toLowerCase().includes(query)
    ).slice(0, 5);
    const customers = data.customers.filter(c =>
      (c.nom || '').toLowerCase().includes(query)
      || (c.prenom || '').toLowerCase().includes(query)
      || (c.email || '').toLowerCase().includes(query)
      || (c.tel || '').toLowerCase().includes(query)
    ).slice(0, 5);
    return { orders, products, customers };
  }, [query, data]);

  const totalMatches = matches.orders.length + matches.products.length + matches.customers.length;
  const showDrop = open && query.length >= 2;

  const go = (route) => { setOpen(false); setQ(''); onNav(route); };

  return (
    <div className="adm-search" ref={wrapRef} style={{ position: 'relative' }}>
      <Icon name="search" size={16}/>
      <input
        placeholder="Rechercher commande, produit, client…"
        value={q}
        onChange={e => { setQ(e.target.value); setOpen(true); }}
        onFocus={() => setOpen(true)}
      />
      {showDrop && (
        <div className="adm-search-drop">
          {totalMatches === 0 && <div className="adm-search-empty">Aucun résultat pour « {q} »</div>}
          {matches.orders.length > 0 && (
            <div className="adm-search-section">
              <div className="adm-search-head">Commandes</div>
              {matches.orders.map(o => (
                <button key={o.id} className="adm-search-item" onClick={() => go('admin-orders')}>
                  <Icon name="box" size={14}/>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div className="adm-search-title">{o.numero || o.id} — {o.client || '—'}</div>
                    <div className="adm-search-sub">{o.statut} · {fmt((Number(o.total) || 0) + (Number(o.livraison) || 0))} TND</div>
                  </div>
                </button>
              ))}
            </div>
          )}
          {matches.products.length > 0 && (
            <div className="adm-search-section">
              <div className="adm-search-head">Produits</div>
              {matches.products.map(p => (
                <button key={p.id} className="adm-search-item" onClick={() => go('admin-products')}>
                  <Icon name="tag" size={14}/>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div className="adm-search-title">{p.nom}</div>
                    <div className="adm-search-sub">{p.marque || '—'} · Réf {p.ref || '—'} · {stockStatusInfo(p.stockStatus).label}</div>
                  </div>
                </button>
              ))}
            </div>
          )}
          {matches.customers.length > 0 && (
            <div className="adm-search-section">
              <div className="adm-search-head">Clients</div>
              {matches.customers.map(c => (
                <button key={c.id} className="adm-search-item" onClick={() => go('admin-customers')}>
                  <Icon name="users" size={14}/>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div className="adm-search-title">{`${c.prenom || ''} ${c.nom || ''}`.trim() || '—'}</div>
                    <div className="adm-search-sub">{c.email || '—'} · {c.tel || '—'}</div>
                  </div>
                </button>
              ))}
            </div>
          )}
        </div>
      )}
    </div>
  );
};

// ==================================================================
//  Admin Notifications — pending orders to confirm
// ==================================================================
const AdminNotifBell = ({ onNav }) => {
  // Single queue: orders awaiting confirmation.
  const [pendingOrders, setPendingOrders] = useAs([]);
  const [open, setOpen] = useAs(false);
  const [busy, setBusy] = useAs({});
  const seenOrdersRef = useAr(null);
  const wrapRef = useAr(null);

  const load = async (notify = true) => {
    try {
      const orderRows = await window.WS_DB.listOrders().catch(() => []);
      const o = (orderRows || []).filter(x => x.statut === 'En attente');
      const oIds = new Set(o.map(x => x.id));
      if (notify && seenOrdersRef.current) {
        const fresh = o.filter(x => !seenOrdersRef.current.has(x.id));
        if (fresh.length) pushToast(`${fresh.length} nouvelle${fresh.length > 1 ? 's' : ''} commande${fresh.length > 1 ? 's' : ''} à confirmer`);
      }
      seenOrdersRef.current = oIds;
      setPendingOrders(o);
    } catch (_) {}
  };

  useAe(() => {
    load(false);
    const t = setInterval(() => load(true), 15000);
    const onChange = () => load(true);
    window.addEventListener('ws-orders-changed', onChange);
    return () => {
      clearInterval(t);
      window.removeEventListener('ws-orders-changed', onChange);
    };
  }, []);

  useAe(() => {
    const h = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, []);

  const confirmOrder = async (o) => {
    const id = o.id;
    setBusy(b => ({ ...b, [id]: true }));
    try {
      await window.WS_DB.updateOrderStatus(id, 'Préparation');
      setPendingOrders(list => list.filter(x => x.id !== id));
      pushToast(`Commande ${o.numero || id} confirmée`);
    } catch(e) {
      pushToast('Erreur : ' + e.message, 'err');
    } finally {
      setBusy(b => { const n = { ...b }; delete n[id]; return n; });
    }
  };

  const rejectOrder = async (o) => {
    const id = o.id;
    if (!confirm(`Annuler la commande ${o.numero || id} ?`)) return;
    setBusy(b => ({ ...b, [id]: true }));
    try {
      await window.WS_DB.updateOrderStatus(id, 'Annulée');
      setPendingOrders(list => list.filter(x => x.id !== id));
      pushToast(`Commande ${o.numero || id} annulée`);
    } catch(e) {
      pushToast('Erreur : ' + e.message, 'err');
    } finally {
      setBusy(b => { const n = { ...b }; delete n[id]; return n; });
    }
  };

  const count = pendingOrders.length;

  return (
    <div ref={wrapRef} style={{ position: 'relative' }}>
      <button className="adm-ico" onClick={() => setOpen(v => !v)}
        title={count ? `${count} notification${count > 1 ? 's' : ''}` : 'Aucune notification'}>
        <Icon name="bell" size={18}/>
        {count > 0 && <span className="adm-notif-dot">{count > 9 ? '9+' : count}</span>}
      </button>
      {open && (
        <div className="adm-notif-drop">
          {/* ----- Orders to confirm ----- */}
          <div className="adm-notif-head">
            <strong>Commandes à confirmer</strong>
            <span className="ws-muted" style={{ fontSize: 12 }}>{pendingOrders.length} en attente</span>
          </div>
          {pendingOrders.length === 0 && <div className="adm-notif-empty">Aucune commande en attente</div>}
          {pendingOrders.slice(0, 6).map(o => {
            const total = (Number(o.total) || 0) + (Number(o.livraison) || 0);
            const items = Array.isArray(o.items) ? o.items.length : 0;
            const b = !!busy[o.id];
            return (
              <div key={o.id} className="adm-notif-item">
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div className="adm-search-title">{o.numero || o.id} — {o.client || '—'}</div>
                  <div className="adm-search-sub">{items} article{items > 1 ? 's' : ''} · {fmt(total)} TND · {o.paiement || 'COD'}</div>
                </div>
                <button className="ws-btn ws-btn-primary ws-btn-sm" disabled={b} onClick={() => confirmOrder(o)}>
                  <Icon name="check" size={12}/> OK
                </button>
                <button className="ws-btn ws-btn-ghost ws-btn-sm" disabled={b} onClick={() => rejectOrder(o)}>
                  <Icon name="close" size={12}/>
                </button>
              </div>
            );
          })}
          {pendingOrders.length > 0 && (
            <button className="adm-notif-foot" onClick={() => { setOpen(false); onNav('admin-orders'); }}>
              Voir toutes les commandes <Icon name="chevR" size={12}/>
            </button>
          )}
        </div>
      )}
    </div>
  );
};

// ==================================================================
//  Featured product selector (appears on client home hero)
// ==================================================================
const FeaturedProductCard = ({ products }) => {
  const [selectedId, setSelectedId] = useAs(null);
  const [search, setSearch] = useAs('');
  const [saving, setSaving] = useAs(false);

  useAe(() => {
    window.WS_DB.getSetting('hero_product_id').then(v => setSelectedId(v || ''));
  }, []);

  const filtered = products.filter(p => {
    if (!search) return true;
    const q = search.toLowerCase();
    return (p.nom || '').toLowerCase().includes(q)
        || (p.ref || '').toLowerCase().includes(q);
  }).slice(0, 30);

  const save = async (id) => {
    setSaving(true);
    try {
      await window.WS_DB.setSetting('hero_product_id', id || null);
      setSelectedId(id || '');
      pushToast(id ? 'Produit vedette mis à jour' : 'Produit vedette retiré');
    } catch (e) {
      pushToast('Erreur : ' + e.message, 'err');
    } finally { setSaving(false); }
  };

  const selected = products.find(p => p.id === selectedId);

  return (
    <div className="adm-card adm-wide">
      <div className="adm-card-head">
        <div>
          <h3>Produit vedette (page d'accueil)</h3>
          <span className="ws-muted">Ce produit apparaît dans la carte "Promo de la semaine" du site client</span>
        </div>
        {selectedId && <button className="ws-btn ws-btn-ghost ws-btn-sm" onClick={()=>save(null)}>Retirer</button>}
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '220px 1fr', gap: 20, alignItems: 'flex-start' }}>
        <div>
          {selected ? (
            <div style={{ border: '2px solid var(--accent)', padding: 12, background: 'var(--bg-2)' }}>
              <div style={{ aspectRatio: '1', background: 'var(--bg)', marginBottom: 10, overflow: 'hidden' }}>
                {selected.image_url
                  ? <img src={selected.image_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}/>
                  : <ProductImg cat={selected.cat} id={selected.id}/>}
              </div>
              <div style={{ fontSize: 11, color: 'var(--muted)' }}>{selected.marque} · {selected.ref}</div>
              <div style={{ fontWeight: 600, fontSize: 13, margin: '4px 0' }}>{selected.nom}</div>
              <div style={{ color: 'var(--accent)', fontWeight: 700 }}>{fmt(selected.prix)} TND</div>
            </div>
          ) : (
            <div style={{ aspectRatio: '1', border: '2px dashed var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--muted)', fontSize: 13, textAlign: 'center', padding: 16 }}>
              Aucun produit vedette sélectionné. Le site affiche par défaut une promo.
            </div>
          )}
        </div>
        <div>
          <div className="adm-search-inline" style={{ marginBottom: 10 }}>
            <Icon name="search" size={14}/>
            <input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Rechercher un produit à mettre en vedette..."/>
          </div>
          <div style={{ maxHeight: 280, overflowY: 'auto', border: '1px solid var(--border)' }}>
            {filtered.map(p => (
              <div
                key={p.id}
                onClick={()=>!saving && save(p.id)}
                style={{
                  display: 'flex', alignItems: 'center', gap: 10, padding: 8,
                  borderBottom: '1px solid var(--border)', cursor: saving ? 'wait' : 'pointer',
                  background: p.id === selectedId ? 'var(--accent-soft)' : 'transparent',
                }}
                onMouseEnter={e => { if (p.id !== selectedId) e.currentTarget.style.background = 'var(--bg-2)'; }}
                onMouseLeave={e => { if (p.id !== selectedId) e.currentTarget.style.background = 'transparent'; }}
              >
                <div className="adm-alert-thumb" style={{ width: 36, height: 36 }}>
                  {p.image_url ? <img src={p.image_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}/> : <ProductImg cat={p.cat} id={p.id}/>}
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 13, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.nom}</div>
                  <div style={{ fontSize: 11, color: 'var(--muted)' }}>{p.marque} · Réf {p.ref}</div>
                </div>
                <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--accent)' }}>{fmt(p.prix)}</div>
                {p.id === selectedId && <Icon name="check" size={16} style={{ color: 'var(--accent)' }}/>}
              </div>
            ))}
            {filtered.length === 0 && <div className="ws-muted" style={{ padding: 20, textAlign: 'center', fontSize: 13 }}>Aucun produit</div>}
          </div>
        </div>
      </div>
    </div>
  );
};

// ==================================================================
//  Dashboard
// ==================================================================
const AdminDashboard = ({ onNav }) => {
  // Start empty — never seed with the bundled demo data. KPIs reflect the
  // real database, including a clean empty state right after a wipe.
  const [products, setProducts] = useAs([]);
  const [orders, setOrders]     = useAs([]);
  const [cats, setCats]         = useAs([]);

  useAe(() => {
    let alive = true;
    Promise.all([
      window.WS_DB.listProducts({ activeOnly: false }),
      window.WS_DB.listOrders(),
      window.WS_DB.listCategories(),
    ]).then(([p, o, c]) => {
      if (!alive) return;
      setProducts(p || []);
      setCats(c || []);
      setOrders((o || []).map(r => ({
        id: r.numero || r.id,
        client: r.client,
        date: r.date && String(r.date).includes('T')
          ? new Date(r.date).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' })
          : r.date,
        total: Number(r.total) + Number(r.livraison || 0),
        statut: r.statut,
        items: Array.isArray(r.items) ? r.items.length : (r.items || 0),
        paiement: r.paiement || 'COD',
      })));
    }).catch(() => {});
    return () => { alive = false; };
  }, []);

  const totalRevenue = orders.reduce((a, o) => a + (Number(o.total) || 0), 0);
  const needsAttention = products.filter(p => p.stockStatus === 'rupture' || p.stockStatus === 'en_arrivage').slice(0, 5);

  // 12-week revenue series from real orders (week buckets)
  const now = new Date();
  const series = Array.from({ length: 12 }, (_, i) => {
    const weekStart = new Date(now);
    weekStart.setDate(weekStart.getDate() - (11 - i) * 7);
    const weekEnd = new Date(weekStart); weekEnd.setDate(weekEnd.getDate() + 7);
    return orders
      .filter(o => {
        const d = o._raw?.date || o.date;
        if (!d) return false;
        const dt = new Date(d);
        return dt >= weekStart && dt < weekEnd;
      })
      .reduce((a, o) => a + (Number(o.total) || 0), 0);
  });
  const maxR = Math.max(1, ...series);

  // live category distribution
  const palette = ['#E8652A','#1E3A5F','#F4A261','#4A6B8A','#E8B27A','#B8C5D1','#7A4AE8','#159F6A'];
  const catData = cats.map((c, i) => {
    const count = products.filter(p => p.cat === c.id).length;
    return { nom: c.nom, pct: products.length ? Math.round((count / products.length) * 100) : 0, color: palette[i % palette.length] };
  }).filter(c => c.pct > 0);

  return (
    <div className="adm-dashboard">
      <div className="adm-page-head">
        <div>
          <h1>Tableau de bord</h1>
          <p className="ws-muted">Aperçu en temps réel · {new Date().toLocaleDateString('fr-FR', { day: '2-digit', month: 'long', year: 'numeric' })}</p>
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          <button className="ws-btn ws-btn-ghost" onClick={()=>onNav('admin-reports')}><Icon name="chart" size={14}/> Rapports</button>
          <button className="ws-btn ws-btn-primary" onClick={()=>onNav('admin-products')}><Icon name="plus" size={14}/> Nouveau produit</button>
        </div>
      </div>

      <div className="adm-kpis">
        {[
          { l: "Chiffre d'affaires", v: fmt(totalRevenue) + ' TND', d: orders.length + ' commandes', dir: 'up', i: 'chart', series: series.length ? series : [0,0,0,0,0,0,0,0,0,0,0,1] },
          { l: 'Commandes',          v: String(orders.length), d: 'au total', dir: 'up', i: 'box', series: series.map(s => s > 0 ? 1 : 0) },
          { l: 'Produits',           v: String(products.length), d: products.length + ' réf.', dir: 'up', i: 'tag', series: [products.length] },
          { l: 'Rupture / arrivage', v: String(products.filter(p => p.stockStatus === 'rupture' || p.stockStatus === 'en_arrivage').length), d: 'à réapprovisionner', dir: 'down', i: 'warn', series: [products.filter(p => p.stockStatus === 'rupture' || p.stockStatus === 'en_arrivage').length] },
        ].map(k => (
          <div key={k.l} className="adm-kpi">
            <div className="adm-kpi-top">
              <div className="adm-kpi-icon"><Icon name={k.i} size={18}/></div>
              <span className={`adm-delta ${k.dir==='up'?'up':'down'}`}>
                <Icon name={k.dir==='up'?'chevU':'chevD'} size={10}/> {k.d}
              </span>
            </div>
            <div className="adm-kpi-value">{k.v}</div>
            <div className="adm-kpi-label">{k.l}</div>
            <Sparkline data={k.series} color={k.dir==='up'?'var(--accent)':'var(--danger)'}/>
          </div>
        ))}
      </div>

      <div className="adm-row">
        <div className="adm-card adm-chart-card">
          <div className="adm-card-head">
            <div><h3>Chiffre d'affaires</h3><span className="ws-muted">12 dernières semaines</span></div>
          </div>
          <BarChart data={series} max={maxR}/>
        </div>

        <div className="adm-card">
          <div className="adm-card-head">
            <h3>Répartition par rayon</h3>
          </div>
          {catData.length > 0 ? (
            <>
              <DonutChart data={catData}/>
              <div className="adm-legend">
                {catData.map(c => (
                  <div key={c.nom} className="adm-legend-item">
                    <span className="adm-legend-dot" style={{ background: c.color }}/>
                    <span>{c.nom}</span>
                    <strong>{c.pct}%</strong>
                  </div>
                ))}
              </div>
            </>
          ) : (
            <p className="ws-muted" style={{ padding: 20, textAlign: 'center' }}>Aucun produit classé.</p>
          )}
        </div>
      </div>

      <div className="adm-row">
        <FeaturedProductCard products={products}/>
      </div>

      <div className="adm-row">
        <div className="adm-card adm-wide">
          <div className="adm-card-head">
            <h3>Commandes récentes</h3>
            <button className="ws-link" onClick={()=>onNav('admin-orders')}>Tout voir <Icon name="chevR" size={12}/></button>
          </div>
          <table className="ws-table adm-table">
            <thead><tr><th>N°</th><th>Client</th><th>Date</th><th>Articles</th><th>Montant</th><th>Statut</th></tr></thead>
            <tbody>
              {orders.slice(0, 6).map(o => (
                <tr key={o.id}>
                  <td><strong>{o.id}</strong></td>
                  <td>{o.client}</td>
                  <td>{o.date}</td>
                  <td>{o.items}</td>
                  <td><strong>{fmt(o.total)} TND</strong></td>
                  <td><span className={`ws-status ws-status-${o.statut?.toLowerCase().replace(/[^a-z]/g,'')}`}>{o.statut}</span></td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>

        <div className="adm-card">
          <div className="adm-card-head">
            <h3>Alertes stock</h3>
            <button className="ws-link" onClick={()=>onNav('admin-stock')}>Gérer</button>
          </div>
          <div className="adm-alerts">
            {needsAttention.map(p => (
              <div key={p.id} className="adm-alert">
                <div className="adm-alert-thumb">
                  {p.image_url ? <img src={p.image_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}/> : <ProductImg cat={p.cat} id={p.id}/>}
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontWeight: 600, fontSize: 13, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.nom}</div>
                  <div className="ws-muted" style={{ fontSize: 11 }}>{p.ref} · {p.marque}</div>
                </div>
                <div style={{ textAlign: 'right' }}>
                  <StockStatusPill status={p.stockStatus}/>
                </div>
              </div>
            ))}
            {!needsAttention.length && <div className="ws-muted" style={{ padding: 20, textAlign: 'center' }}>Tous les stocks sont OK 👍</div>}
          </div>
        </div>
      </div>
    </div>
  );
};

// ==================================================================
//  Charts (unchanged from Phase 1)
// ==================================================================
const Sparkline = ({ data, color = 'var(--accent)' }) => {
  if (!Array.isArray(data) || data.length < 2) return null;
  const max = Math.max(...data);
  const min = Math.min(...data);
  const span = max - min || 1;
  const pts = data
    .map((v, i) => `${(i / (data.length - 1)) * 100},${100 - ((v - min) / span) * 100}`)
    .filter(p => !p.includes('NaN'))
    .join(' ');
  if (!pts) return null;
  return (
    <svg viewBox="0 0 100 100" preserveAspectRatio="none" className="adm-spark">
      <polyline points={pts} fill="none" stroke={color} strokeWidth="2" vectorEffect="non-scaling-stroke"/>
    </svg>
  );
};

const BarChart = ({ data, max }) => (
  <div className="adm-bars">
    {data.map((v, i) => (
      <div key={i} className="adm-bar-col">
        <div className="adm-bar-wrap">
          <div className="adm-bar" style={{ height: (v/max)*100 + '%' }}>
            <span className="adm-bar-tip">{v.toLocaleString('fr')}</span>
          </div>
        </div>
        <span className="adm-bar-label">S{i+1}</span>
      </div>
    ))}
  </div>
);

const DonutChart = ({ data }) => {
  const total = data.reduce((a, b) => a + b.pct, 0);
  let off = 0;
  const R = 60, C = 2 * Math.PI * R;
  return (
    <div className="adm-donut-wrap">
      <svg viewBox="0 0 160 160" width="200" height="200">
        <circle cx="80" cy="80" r={R} fill="none" stroke="var(--border)" strokeWidth="18"/>
        {data.map((d, i) => {
          const len = (d.pct/total) * C;
          const el = <circle key={i} cx="80" cy="80" r={R} fill="none" stroke={d.color} strokeWidth="18"
                              strokeDasharray={`${len} ${C}`} strokeDashoffset={-off}
                              transform="rotate(-90 80 80)" strokeLinecap="butt"/>;
          off += len;
          return el;
        })}
        <text x="80" y="78" textAnchor="middle" fontSize="22" fontWeight="700" fill="var(--fg)">100%</text>
        <text x="80" y="96" textAnchor="middle" fontSize="10" fill="var(--muted)">catégorisé</text>
      </svg>
    </div>
  );
};

// ==================================================================
//  Orders (with live status update)
// ==================================================================
const AdminOrders = () => {
  const [statut, setStatut] = useAs('Tous');
  const [selected, setSelected] = useAs(null);
  const [orders, setOrders] = useAs([]);
  const [loading, setLoading] = useAs(true);
  const [search, setSearch] = useAs('');
  const [page, setPage]     = useAs(1);
  // Date range — defaults to "last 30 days". Used for export + bulk delete.
  const today = new Date();
  const monthAgo = new Date(); monthAgo.setDate(today.getDate() - 30);
  const ymd = (d) => d.toISOString().slice(0, 10);
  const [periodFrom, setPeriodFrom] = useAs(ymd(monthAgo));
  const [periodTo, setPeriodTo]     = useAs(ymd(today));
  const [archiving, setArchiving]   = useAs(false);

  useAe(() => { setPage(1); }, [statut, search]);

  const load = () => {
    setLoading(true);
    return window.WS_DB.listOrders().then(rows => {
      const mapped = rows.map(r => ({
        id: r.numero || r.id,
        dbId: r.id, // uuid for updates
        client: r.client,
        date: r.date && String(r.date).includes('T')
          ? new Date(r.date).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' })
          : r.date,
        total: Number(r.total) + Number(r.livraison || 0),
        statut: r.statut,
        items: Array.isArray(r.items) ? r.items : [],
        paiement: r.paiement || 'COD',
        _raw: r,
      }));
      setOrders(mapped);
    }).catch(() => {}).finally(() => setLoading(false));
  };

  useAe(() => { load(); }, []);

  const items = orders.filter(o => {
    if (statut !== 'Tous' && o.statut !== statut) return false;
    if (!search) return true;
    const q = search.toLowerCase();
    return (o.id || '').toLowerCase().includes(q)
        || (o.client || '').toLowerCase().includes(q)
        || (o._raw?.email || '').toLowerCase().includes(q)
        || (o._raw?.tel || '').toLowerCase().includes(q);
  });
  const paged = items.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);

  // ---- Period helpers -------------------------------------------------
  // Filter the loaded orders to those whose `date` field falls inside the
  // user-picked [periodFrom, periodTo] window (inclusive on both ends).
  const inPeriod = (o) => {
    const raw = o._raw?.date || o.date;
    if (!raw) return false;
    const d = new Date(raw);
    if (isNaN(d.getTime())) return false;
    const start = new Date(periodFrom + 'T00:00:00');
    const end   = new Date(periodTo   + 'T23:59:59');
    return d >= start && d <= end;
  };
  const periodOrders = orders.filter(inPeriod);

  // ---- CSV export ----------------------------------------------------
  // One row per product line: order #, date, client, contact, address,
  // status, payment method, product ref/name, qty, unit price, line total,
  // order total. So each order spans N rows (one per item) — best for
  // Excel pivots / accounting tools.
  const exportCsv = () => {
    if (!periodOrders.length) return pushToast('Aucune commande dans la période', 'err');
    const esc = (v) => {
      const s = v == null ? '' : String(v);
      return /[",;\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
    };
    const header = ['Numero','Date','Statut','Client','Email','Telephone','Adresse','Ville','CP','Paiement','Notes','Ref','Produit','Qte','PrixUnitaire','TotalLigne','Livraison','TotalCommande'];
    const rows = [header.join(';')];
    for (const o of periodOrders) {
      const r = o._raw || {};
      const its = Array.isArray(r.items) ? r.items : [];
      const livraison = Number(r.livraison || 0);
      const total = Number(r.total || 0) + livraison;
      if (!its.length) {
        rows.push([
          r.numero||o.id, r.date||'', r.statut||'', r.client||'', r.email||'',
          r.tel||'', r.adresse||'', r.ville||'', r.cp||'', r.paiement||'', r.notes||'',
          '','','','','', livraison, total,
        ].map(esc).join(';'));
        continue;
      }
      for (const it of its) {
        const lineTotal = (Number(it.prix||0)) * (Number(it.qte||0));
        rows.push([
          r.numero||o.id, r.date||'', r.statut||'', r.client||'', r.email||'',
          r.tel||'', r.adresse||'', r.ville||'', r.cp||'', r.paiement||'', r.notes||'',
          it.ref||'', it.nom||'', it.qte||'', it.prix||'', lineTotal,
          livraison, total,
        ].map(esc).join(';'));
      }
    }
    const blob = new Blob(['﻿' + rows.join('\n')], { type: 'text/csv;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `wattspot-commandes-${periodFrom}-${periodTo}.csv`;
    a.click();
    URL.revokeObjectURL(url);
    pushToast(`${periodOrders.length} commande${periodOrders.length>1?'s':''} exportée${periodOrders.length>1?'s':''}`);
  };

  // ---- PDF export ----------------------------------------------------
  // Opens a print-ready HTML doc in a new tab; the user clicks the print
  // button → "Save as PDF" from the browser's print dialog. No PDF lib
  // dependency, works offline.
  const exportPdf = () => {
    if (!periodOrders.length) return pushToast('Aucune commande dans la période', 'err');
    const esc = (s) => String(s == null ? '' : s)
      .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
      .replace(/"/g,'&quot;');
    const totalAll = periodOrders.reduce((acc, o) => {
      const r = o._raw || {};
      return acc + (Number(r.total||0) + Number(r.livraison||0));
    }, 0);
    const docHtml = `<!doctype html><html lang="fr"><head><meta charset="utf-8">
      <title>Commandes Watt Spot · ${esc(periodFrom)} → ${esc(periodTo)}</title>
      <style>
        body { font-family: -apple-system, Segoe UI, sans-serif; padding: 24px; color: #222; }
        h1 { color: #E8652A; margin: 0 0 4px; }
        .sub { color: #666; font-size: 13px; margin-bottom: 18px; }
        .actions { margin-bottom: 18px; }
        .actions button { padding: 8px 16px; font-weight: 600; background: #E8652A; color: #fff; border: 0; cursor: pointer; }
        .order { border: 1px solid #ddd; margin-bottom: 14px; page-break-inside: avoid; }
        .order-head { background: #f6f6f6; padding: 10px 14px; display: flex; justify-content: space-between; font-size: 13px; }
        .order-head strong { font-size: 14px; }
        .meta { padding: 8px 14px; font-size: 12px; color: #444; }
        table { width: 100%; border-collapse: collapse; }
        th, td { text-align: left; padding: 6px 14px; font-size: 12px; border-top: 1px solid #eee; }
        th { background: #fafafa; font-weight: 600; }
        td.num, th.num { text-align: right; }
        .totals { padding: 8px 14px; font-size: 13px; text-align: right; }
        .summary { background: #fff7f0; border: 1px solid #E8652A; padding: 12px 14px; margin-bottom: 18px; font-weight: 600; }
        @media print { .actions { display: none; } }
      </style></head><body>
      <h1>Watt Spot · Commandes</h1>
      <div class="sub">Période ${esc(periodFrom)} → ${esc(periodTo)} · ${periodOrders.length} commande(s)</div>
      <div class="actions">
        <button onclick="window.print()">🖨 Imprimer / Enregistrer en PDF</button>
      </div>
      <div class="summary">Chiffre d'affaires période : ${totalAll.toFixed(3)} TND</div>
      ${periodOrders.map(o => {
        const r = o._raw || {};
        const its = Array.isArray(r.items) ? r.items : [];
        const liv = Number(r.livraison||0);
        const tot = Number(r.total||0) + liv;
        return `<div class="order">
          <div class="order-head">
            <strong>${esc(r.numero||o.id)}</strong>
            <span>${esc(r.date||'')} · ${esc(r.statut||'')}</span>
          </div>
          <div class="meta">
            <div><strong>${esc(r.client||'')}</strong> · ${esc(r.tel||'')} · ${esc(r.email||'')}</div>
            ${r.adresse ? `<div>${esc(r.adresse)}${r.ville ? `, ${esc(r.ville)}` : ''}${r.cp ? ` ${esc(r.cp)}` : ''}</div>` : ''}
            ${r.notes ? `<div style="font-style:italic;color:#666">Notes : ${esc(r.notes)}</div>` : ''}
          </div>
          <table>
            <thead><tr><th>Réf</th><th>Produit</th><th class="num">Qté</th><th class="num">Prix</th><th class="num">Total</th></tr></thead>
            <tbody>
              ${its.map(it => `<tr>
                <td>${esc(it.ref||'')}</td>
                <td>${esc(it.nom||'')}</td>
                <td class="num">${esc(it.qte||0)}</td>
                <td class="num">${Number(it.prix||0).toFixed(3)}</td>
                <td class="num">${(Number(it.prix||0)*Number(it.qte||0)).toFixed(3)}</td>
              </tr>`).join('')}
            </tbody>
          </table>
          <div class="totals">
            Livraison : ${liv.toFixed(3)} TND · <strong>Total : ${tot.toFixed(3)} TND</strong>
          </div>
        </div>`;
      }).join('')}
      </body></html>`;
    const win = window.open('', '_blank');
    if (!win) return pushToast('Pop-up bloqué — autorisez les fenêtres pour ce site', 'err');
    win.document.open();
    win.document.write(docHtml);
    win.document.close();
  };

  // ---- Bulk delete by period ----------------------------------------
  const archivePeriod = async () => {
    if (!periodOrders.length) return pushToast('Aucune commande dans la période', 'err');
    const msg = `Supprimer définitivement ${periodOrders.length} commande${periodOrders.length>1?'s':''} de la période ` +
                `${periodFrom} → ${periodTo} ?\n\n⚠️ Exportez d'abord en CSV / PDF — cette action est irréversible.`;
    if (!window.confirm(msg)) return;
    setArchiving(true);
    try {
      const ids = periodOrders.map(o => o._raw?.id || o.dbId).filter(Boolean);
      const n = await window.WS_DB.deleteOrders(ids);
      pushToast(`${n} commande${n>1?'s':''} supprimée${n>1?'s':''}`);
      await load();
    } catch (e) {
      pushToast('Erreur : ' + e.message, 'err');
    } finally {
      setArchiving(false);
    }
  };

  if (selected) return <OrderDetail order={selected} onBack={()=>{ setSelected(null); load(); }}/>;

  const counts = {
    'Tous': orders.length,
    'En attente': orders.filter(o=>o.statut==='En attente').length,
    'Préparation': orders.filter(o=>o.statut==='Préparation').length,
    'Expédiée': orders.filter(o=>o.statut==='Expédiée').length,
    'Livrée': orders.filter(o=>o.statut==='Livrée').length,
    'Annulée': orders.filter(o=>o.statut==='Annulée').length,
  };

  return (
    <div>
      <div className="adm-page-head">
        <div>
          <h1>Commandes</h1>
          <p className="ws-muted">{orders.length} commandes · {window.WS_DB.mode()==='live'?'connecté à Supabase':'mode démo (localStorage)'}</p>
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          <button className="ws-btn ws-btn-ghost" onClick={load}><Icon name="refresh" size={14}/> Actualiser</button>
        </div>
      </div>

      {/* Period toolbar — export + bulk delete by date range.
          Lets the admin archive old commands as CSV/PDF and free up the
          Supabase database (free-tier storage is limited).               */}
      <div className="adm-card" style={{ padding: 14, marginBottom: 14 }}>
        <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'flex-end' }}>
          <div>
            <div className="ws-muted" style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase', marginBottom: 4 }}>Période</div>
            <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
              <input type="date" value={periodFrom} onChange={e=>setPeriodFrom(e.target.value)}
                style={{ padding: '8px 10px', border: '1px solid var(--border)', background: 'var(--bg-2)', fontFamily: 'inherit', fontSize: 13 }}/>
              <span className="ws-muted" style={{ fontSize: 13 }}>→</span>
              <input type="date" value={periodTo} onChange={e=>setPeriodTo(e.target.value)}
                style={{ padding: '8px 10px', border: '1px solid var(--border)', background: 'var(--bg-2)', fontFamily: 'inherit', fontSize: 13 }}/>
            </div>
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' }}>
            <div className="ws-muted" style={{ fontSize: 12 }}>
              <strong style={{ color: 'var(--accent)' }}>{periodOrders.length}</strong> commande{periodOrders.length>1?'s':''} dans la période
            </div>
          </div>
          <div style={{ marginLeft: 'auto', display: 'flex', gap: 8, flexWrap: 'wrap' }}>
            <button className="ws-btn ws-btn-ghost" onClick={exportCsv}
              disabled={!periodOrders.length}
              title="Télécharger les commandes de la période en CSV (Excel)">
              <Icon name="download" size={14}/> CSV
            </button>
            <button className="ws-btn ws-btn-ghost" onClick={exportPdf}
              disabled={!periodOrders.length}
              title="Aperçu PDF imprimable des commandes de la période">
              <Icon name="download" size={14}/> PDF
            </button>
            <button
              className="ws-btn"
              style={{ background: 'var(--danger)', borderColor: 'var(--danger)', color: '#fff' }}
              onClick={archivePeriod}
              disabled={!periodOrders.length || archiving}
              title="Supprime les commandes de la période (à faire après export pour libérer Supabase)">
              <Icon name="trash" size={14}/> {archiving ? 'Suppression…' : 'Supprimer la période'}
            </button>
          </div>
        </div>
        <p className="ws-muted" style={{ fontSize: 11.5, marginTop: 10, marginBottom: 0 }}>
          Exportez d'abord en CSV ou PDF, puis supprimez pour libérer la base de données Supabase (quota gratuit limité).
        </p>
      </div>

      <div className="adm-pills">
        {Object.entries(counts).map(([k, v]) => (
          <button key={k} className={statut===k?'active':''} onClick={()=>setStatut(k)}>
            {k} <span>{v}</span>
          </button>
        ))}
      </div>

      <div className="adm-card">
        <div className="adm-toolbar">
          <div className="adm-search-inline">
            <Icon name="search" size={14}/>
            <input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Chercher N°, client, email, téléphone..."/>
          </div>
        </div>
        <table className="ws-table adm-table">
          <thead>
            <tr>
              <th>N° Commande</th><th>Client</th><th>Date</th><th>Articles</th>
              <th>Paiement</th><th>Montant</th><th>Statut</th><th></th>
            </tr>
          </thead>
          <tbody>
            {paged.map(o => (
              <tr key={o.id} onClick={()=>setSelected(o)} style={{ cursor: 'pointer' }}>
                <td><strong>{o.id}</strong></td>
                <td>{o.client}</td>
                <td>{o.date}</td>
                <td>{Array.isArray(o.items) ? o.items.length : o.items}</td>
                <td>{o.paiement}</td>
                <td><strong>{fmt(o.total)} TND</strong></td>
                <td><span className={`ws-status ws-status-${o.statut?.toLowerCase().replace(/[^a-z]/g,'')}`}>{o.statut}</span></td>
                <td><button className="adm-row-btn"><Icon name="chevR" size={14}/></button></td>
              </tr>
            ))}
            {!loading && !items.length && (
              <tr><td colSpan="8" style={{ textAlign: 'center', padding: 40 }} className="ws-muted">Aucune commande</td></tr>
            )}
          </tbody>
        </table>
        <Pagination page={page} total={items.length} onPage={setPage}/>
      </div>
    </div>
  );
};

const OrderDetail = ({ order, onBack }) => {
  const [statut, setStatut] = useAs(order.statut);
  const [saving, setSaving] = useAs(false);
  const dbId = order.dbId || order._raw?.id || order.id;

  const updateStatus = async (newStatus) => {
    setSaving(true);
    try {
      await window.WS_DB.updateOrderStatus(dbId, newStatus);
      setStatut(newStatus);
      pushToast('Statut mis à jour : ' + newStatus);
    } catch (e) {
      pushToast('Erreur : ' + e.message, 'err');
    } finally { setSaving(false); }
  };

  const items = Array.isArray(order.items) ? order.items : [];

  return (
    <div>
      <button className="ws-link-sm" onClick={onBack} style={{ marginBottom: 12 }}><Icon name="chevL" size={12}/> Retour aux commandes</button>
      <div className="adm-page-head">
        <div>
          <h1>{order.id}</h1>
          <p className="ws-muted">{order.date} · {order.client} · <span className={`ws-status ws-status-${statut?.toLowerCase().replace(/[^a-z]/g,'')}`}>{statut}</span></p>
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          {statut === 'En attente' && <button disabled={saving} className="ws-btn ws-btn-primary" onClick={()=>updateStatus('Préparation')}>Passer en préparation</button>}
          {statut === 'Préparation' && <button disabled={saving} className="ws-btn ws-btn-primary" onClick={()=>updateStatus('Expédiée')}>Marquer expédiée <Icon name="truck" size={14}/></button>}
          {statut === 'Expédiée' && <button disabled={saving} className="ws-btn ws-btn-primary" onClick={()=>updateStatus('Livrée')}>Marquer livrée <Icon name="check" size={14}/></button>}
          {!['Annulée','Livrée'].includes(statut) && <button disabled={saving} className="ws-btn ws-btn-ghost" onClick={()=>updateStatus('Annulée')}>Annuler</button>}
        </div>
      </div>

      <div className="adm-timeline">
        {['Commandée','En préparation','Expédiée','Livrée'].map((s, i) => {
          const level = statut==='Annulée' ? 0
                      : statut==='Livrée' ? 4
                      : statut==='Expédiée' ? 3
                      : statut==='Préparation' ? 2
                      : 1;
          const done = i < level;
          return (
            <div key={s} className={`adm-step ${done?'done':''}`}>
              <div className="adm-step-dot">{done ? <Icon name="check" size={12}/> : i+1}</div>
              <span>{s}</span>
            </div>
          );
        })}
      </div>

      <div className="adm-row">
        <div className="adm-card adm-wide">
          <h3>Articles ({items.length})</h3>
          <table className="ws-table adm-table">
            <thead><tr><th>Produit</th><th>Réf</th><th>Qté</th><th>Prix</th><th>Total</th></tr></thead>
            <tbody>
              {items.map((it, i) => (
                <tr key={it.id || i}>
                  <td><div style={{ fontWeight: 600 }}>{it.nom}</div></td>
                  <td className="adm-mono">{it.ref || '—'}</td>
                  <td>{it.qte}</td>
                  <td>{fmt(it.prix)}</td>
                  <td><strong>{fmt(it.prix * it.qte)}</strong></td>
                </tr>
              ))}
              {!items.length && <tr><td colSpan="5" className="ws-muted" style={{ textAlign: 'center', padding: 20 }}>Articles non détaillés</td></tr>}
            </tbody>
          </table>
          <div className="adm-order-totals">
            <div className="ws-sum-row"><span>Sous-total</span><span>{fmt(order._raw?.total || order.total)} TND</span></div>
            <div className="ws-sum-row"><span>Livraison</span><span>{fmt(order._raw?.livraison || 0)} TND</span></div>
            <div className="ws-sum-total"><span>Total</span><strong>{fmt(order.total)} TND</strong></div>
          </div>
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
          <div className="adm-card">
            <h3>Client</h3>
            <div className="adm-client-card">
              <div className="ws-avatar">{(order.client || '??').split(' ').map(x=>x[0]).join('').slice(0,2)}</div>
              <div>
                <strong>{order.client}</strong>
                <div className="ws-muted" style={{ fontSize: 12 }}>{order._raw?.email || ''}</div>
              </div>
            </div>
            <div className="adm-mini-rows">
              {order._raw?.email && <div><Icon name="mail" size={12}/> {order._raw.email}</div>}
              {order._raw?.tel && <div><Icon name="phone" size={12}/> {order._raw.tel}</div>}
            </div>
          </div>
          <div className="adm-card">
            <h3>Livraison</h3>
            <p style={{ fontSize: 13, lineHeight: 1.6 }}>
              {order._raw?.adresse || '—'}<br/>
              {order._raw?.ville} {order._raw?.cp}<br/>
              Tunisie
            </p>
          </div>
          <div className="adm-card">
            <h3>Paiement</h3>
            <p style={{ fontSize: 13 }}>{order.paiement}</p>
          </div>
        </div>
      </div>
    </div>
  );
};

// ==================================================================
//  Stock (3-state availability picker: Disponible / Rupture / En arrivage)
// ==================================================================
const AdminStock = () => {
  const [f, setF] = useAs('all');
  const [products, setProducts] = useAs([]);
  const [savingId, setSavingId] = useAs(null);
  const [search, setSearch] = useAs('');
  const [filterCat, setFilterCat] = useAs('');
  const [filterBrand, setFilterBrand] = useAs('');
  const [priceMin, setPriceMin] = useAs('');
  const [priceMax, setPriceMax] = useAs('');
  const [page, setPage] = useAs(1);
  useAe(() => { setPage(1); }, [search, f, filterCat, filterBrand, priceMin, priceMax]);

  // Same merged-tree categories source as AdminProducts, for the picker.
  const { flat: cats } = useMergedCategories();

  const load = () => window.WS_DB.listProducts({ activeOnly: false }).then(setProducts).catch(() => {});
  useAe(() => { load(); }, []);

  // Distinct brands list for the dropdown.
  const allBrands = [...new Set(products.map(p => (p.marque || '').trim()).filter(Boolean))]
    .sort((a, b) => a.localeCompare(b));

  const minNum = priceMin === '' ? null : Number(priceMin);
  const maxNum = priceMax === '' ? null : Number(priceMax);
  const hasAnyFilter = Boolean(filterCat || filterBrand
    || (minNum != null && !isNaN(minNum))
    || (maxNum != null && !isNaN(maxNum)) || search);

  const filtered = products.filter(p => {
    if (f !== 'all' && (p.stockStatus || 'disponible') !== f) return false;
    // Category / brand / price
    if (filterCat && p.cat !== filterCat) return false;
    if (filterBrand && (p.marque || '').toLowerCase() !== filterBrand.toLowerCase()) return false;
    const prix = Number(p.prix) || 0;
    if (minNum != null && !isNaN(minNum) && prix < minNum) return false;
    if (maxNum != null && !isNaN(maxNum) && prix > maxNum) return false;
    if (!search) return true;
    const q = search.toLowerCase();
    return (p.nom || '').toLowerCase().includes(q)
        || (p.ref || '').toLowerCase().includes(q)
        || (p.marque || '').toLowerCase().includes(q);
  });
  const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);

  const resetFilters = () => {
    setSearch(''); setFilterCat(''); setFilterBrand('');
    setPriceMin(''); setPriceMax('');
  };

  const setStatus = async (id, stockStatus) => {
    setSavingId(id);
    const prev = products;
    setProducts(ps => ps.map(p => p.id === id ? { ...p, stockStatus } : p));
    try {
      await window.WS_DB.updateStockStatus(id, stockStatus);
      pushToast('Disponibilité mise à jour');
    } catch (e) {
      setProducts(prev);
      pushToast('Erreur : ' + e.message, 'err');
    } finally {
      setSavingId(null);
    }
  };

  const countByStatus = (s) => products.filter(p => (p.stockStatus || 'disponible') === s).length;

  return (
    <div>
      <div className="adm-page-head">
        <div><h1>Stock & Disponibilité</h1><p className="ws-muted">{products.length} références</p></div>
        <div style={{ display: 'flex', gap: 8 }}>
          <button className="ws-btn ws-btn-ghost" onClick={load}><Icon name="refresh" size={14}/> Actualiser</button>
        </div>
      </div>

      <div className="adm-kpis">
        <div className="adm-kpi"><div className="adm-kpi-top"><div className="adm-kpi-icon" style={{ background: STOCK_STATUSES.disponible.bg, color: STOCK_STATUSES.disponible.color }}><Icon name="check" size={18}/></div></div><div className="adm-kpi-value">{countByStatus('disponible')}</div><div className="adm-kpi-label">Disponible</div></div>
        <div className="adm-kpi"><div className="adm-kpi-top"><div className="adm-kpi-icon" style={{ background: STOCK_STATUSES.en_arrivage.bg, color: STOCK_STATUSES.en_arrivage.color }}><Icon name="box" size={18}/></div></div><div className="adm-kpi-value">{countByStatus('en_arrivage')}</div><div className="adm-kpi-label">En arrivage</div></div>
        <div className="adm-kpi"><div className="adm-kpi-top"><div className="adm-kpi-icon" style={{ background: STOCK_STATUSES.rupture.bg, color: STOCK_STATUSES.rupture.color }}><Icon name="warn" size={18}/></div></div><div className="adm-kpi-value">{countByStatus('rupture')}</div><div className="adm-kpi-label">Rupture de stock</div></div>
      </div>

      <div className="adm-pills">
        {[
          ['all','Tous',products.length],
          ['disponible','Disponible',countByStatus('disponible')],
          ['en_arrivage','En arrivage',countByStatus('en_arrivage')],
          ['rupture','Rupture',countByStatus('rupture')],
        ].map(([k,l,c]) => (
          <button key={k} className={f===k?'active':''} onClick={()=>setF(k)}>{l} <span>{c}</span></button>
        ))}
      </div>

      <div className="adm-card">
        <div className="adm-toolbar" style={{ flexWrap: 'wrap', gap: 10 }}>
          <div className="adm-search-inline" style={{ flex: '1 1 220px', minWidth: 200 }}>
            <Icon name="search" size={14}/>
            <input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Chercher par nom, réf, marque..."/>
          </div>
          <div style={{ flex: '1 1 220px', minWidth: 200 }}>
            <LeafCategoryPicker cats={cats} value={filterCat}
              onChange={setFilterCat}
              placeholder="Toutes catégories" allowClear/>
          </div>
          <select value={filterBrand} onChange={e=>setFilterBrand(e.target.value)}
            className="ws-select"
            style={{ minWidth: 160, flex: '0 1 180px' }}>
            <option value="">Toutes marques</option>
            {allBrands.map(b => <option key={b} value={b}>{b}</option>)}
          </select>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, flex: '0 1 240px' }}>
            <input type="number" min="0" step="0.001"
              value={priceMin} onChange={e=>setPriceMin(e.target.value)}
              placeholder="Prix min"
              className="ws-select"
              style={{ width: 100, padding: '8px 10px' }}/>
            <span className="ws-muted" style={{ fontSize: 12 }}>–</span>
            <input type="number" min="0" step="0.001"
              value={priceMax} onChange={e=>setPriceMax(e.target.value)}
              placeholder="Prix max"
              className="ws-select"
              style={{ width: 100, padding: '8px 10px' }}/>
            <span className="ws-muted" style={{ fontSize: 11 }}>TND</span>
          </div>
          {hasAnyFilter && (
            <button className="ws-btn ws-btn-ghost" onClick={resetFilters} style={{ padding: '8px 12px' }}>
              <Icon name="close" size={12}/> Réinitialiser
            </button>
          )}
        </div>
        <table className="ws-table adm-table">
          <thead><tr><th>Produit</th><th>Réf</th><th>Catégorie</th><th>Marque</th><th>Prix</th><th>Disponibilité</th></tr></thead>
          <tbody>
            {paged.map(p => {
              const cat = (window.CATEGORIES || []).find(c => c.id === p.cat);
              const status = p.stockStatus || 'disponible';
              const isSaving = savingId === p.id;
              return (
                <tr key={p.id}>
                  <td>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                      <div className="adm-alert-thumb">
                        {p.image_url ? <img src={p.image_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}/> : <ProductImg cat={p.cat} id={p.id}/>}
                      </div>
                      <div style={{ fontWeight: 600, fontSize: 13 }}>{p.nom}</div>
                    </div>
                  </td>
                  <td className="adm-mono">{p.ref}</td>
                  <td>{cat?.nom}</td>
                  <td>{p.marque}</td>
                  <td>{fmt(p.prix)}</td>
                  <td>
                    <div className="adm-stock-choice">
                      {Object.entries(STOCK_STATUSES).map(([k, s]) => (
                        <button key={k} disabled={isSaving}
                          className={status === k ? 'active' : ''}
                          style={status === k ? { background: s.bg, color: s.color } : undefined}
                          onClick={() => setStatus(p.id, k)}>
                          <Icon name={s.icon} size={11}/> {s.label}
                        </button>
                      ))}
                    </div>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
        <Pagination page={page} total={filtered.length} onPage={setPage}/>
      </div>
    </div>
  );
};

// ==================================================================
//  Products CRUD
// ==================================================================
const AdminProducts = ({ initialSearch = '', initialBrand = '' } = {}) => {
  const [editing, setEditing] = useAs(null);
  const [products, setProducts] = useAs([]);
  const [search, setSearch] = useAs(initialSearch);
  const [filterCat, setFilterCat] = useAs('');
  const [filterBrand, setFilterBrand] = useAs(initialBrand);
  const [priceMin, setPriceMin] = useAs('');
  const [priceMax, setPriceMax] = useAs('');
  const [confirm, setConfirm] = useAs(null);
  const [page, setPage] = useAs(1);
  const [selected, setSelected] = useAs(() => new Set());
  const [bulkDeleting, setBulkDeleting] = useAs(false);

  // Single source of truth for categories — bundled scrape tree + admin
  // overrides (the same the public site sees). Forget the legacy Supabase
  // `categories` rows; flat = merged hierarchy.
  const { flat: cats } = useMergedCategories();

  useAe(() => { setPage(1); setSelected(new Set()); }, [search, filterCat, filterBrand, priceMin, priceMax]);

  const load = () => window.WS_DB.listProducts({ activeOnly: false })
    .then(p => { setProducts(p); setSelected(new Set()); }).catch(() => {});

  useAe(() => { load(); }, []);

  const [importing, setImporting] = useAs(false);

  if (editing) return <ProductEditor p={editing} cats={cats} onBack={(reload) => { setEditing(null); if (reload) load(); }}/>;
  if (importing) return <AdminProductImport cats={cats} onBack={(reload) => { setImporting(false); if (reload) load(); }}/>;

  // Distinct brands list (sorted alphabetically) for the dropdown filter.
  const allBrands = [...new Set(products.map(p => (p.marque || '').trim()).filter(Boolean))]
    .sort((a, b) => a.localeCompare(b));

  const minNum = priceMin === '' ? null : Number(priceMin);
  const maxNum = priceMax === '' ? null : Number(priceMax);
  const hasAnyFilter = Boolean(filterCat || filterBrand || (minNum != null && !isNaN(minNum)) || (maxNum != null && !isNaN(maxNum)) || search);

  const filtered = products.filter(p => {
    if (filterCat && p.cat !== filterCat) return false;
    if (filterBrand && (p.marque || '').toLowerCase() !== filterBrand.toLowerCase()) return false;
    const prix = Number(p.prix) || 0;
    if (minNum != null && !isNaN(minNum) && prix < minNum) return false;
    if (maxNum != null && !isNaN(maxNum) && prix > maxNum) return false;
    if (!search) return true;
    const q = search.toLowerCase();
    return (p.nom || '').toLowerCase().includes(q)
        || (p.ref || '').toLowerCase().includes(q)
        || (p.marque || '').toLowerCase().includes(q);
  });

  const resetFilters = () => {
    setSearch(''); setFilterCat(''); setFilterBrand('');
    setPriceMin(''); setPriceMax('');
  };
  const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);

  const onDelete = async (p) => {
    try {
      await window.WS_DB.deleteProduct(p.id);
      setProducts(ps => ps.filter(x => x.id !== p.id));
      pushToast('Produit supprimé');
    } catch (e) {
      pushToast('Erreur : ' + e.message, 'err');
    } finally {
      setConfirm(null);
    }
  };

  // ----- Bulk selection helpers -----
  const toggleOne = (id) => {
    const n = new Set(selected);
    n.has(id) ? n.delete(id) : n.add(id);
    setSelected(n);
  };
  const allOnPageChecked = paged.length > 0 && paged.every(p => selected.has(p.id));
  const someOnPageChecked = paged.some(p => selected.has(p.id));
  const togglePage = () => {
    const n = new Set(selected);
    if (allOnPageChecked) paged.forEach(p => n.delete(p.id));
    else paged.forEach(p => n.add(p.id));
    setSelected(n);
  };
  const selectAllFiltered = () => setSelected(new Set(filtered.map(p => p.id)));
  const clearSelection = () => setSelected(new Set());

  const bulkDelete = async () => {
    if (!selected.size) return;
    if (!window.confirm(`Supprimer ${selected.size} produit${selected.size > 1 ? 's' : ''} ? Cette action est irréversible.`)) return;
    setBulkDeleting(true);
    let ok = 0, fail = 0;
    for (const id of selected) {
      try { await window.WS_DB.deleteProduct(id); ok++; }
      catch (_) { fail++; }
    }
    setBulkDeleting(false);
    setProducts(ps => ps.filter(p => !selected.has(p.id)));
    setSelected(new Set());
    pushToast(`${ok} supprimé${ok > 1 ? 's' : ''}` + (fail ? ` · ${fail} échec${fail > 1 ? 's' : ''}` : ''));
  };

  return (
    <div>
      <div className="adm-page-head">
        <div><h1>Produits</h1><p className="ws-muted">{products.length} produits · {filtered.length} affichés</p></div>
        <div style={{ display: 'flex', gap: 8 }}>
          <button className="ws-btn ws-btn-ghost" onClick={load}><Icon name="refresh" size={14}/> Actualiser</button>
          <button className="ws-btn ws-btn-ghost" onClick={() => setImporting(true)}>
            <Icon name="pkg" size={14}/> Importer en masse
          </button>
          <button className="ws-btn ws-btn-primary" onClick={()=>setEditing({ nouveau: true, nom: '', marque: '', prix: 0, stock: 0, stockStatus: 'disponible', cat: cats[0]?.slug || '', ref: '', desc: '', tag: '', image_url: '', active: true, web_visible: true, mobile_visible: true })}>
            <Icon name="plus" size={14}/> Nouveau produit
          </button>
        </div>
      </div>

      {/* Bulk action bar — visible only when something is selected */}
      {selected.size > 0 && (
        <div className="adm-card" style={{ padding: 12, marginBottom: 12, display: 'flex', alignItems: 'center', gap: 12, background: 'var(--accent-soft)', border: '1px solid var(--accent)' }}>
          <strong style={{ color: 'var(--accent)' }}>{selected.size} produit{selected.size > 1 ? 's' : ''} sélectionné{selected.size > 1 ? 's' : ''}</strong>
          {selected.size < filtered.length && (
            <button className="ws-link-sm" onClick={selectAllFiltered} style={{ cursor: 'pointer' }}>
              Sélectionner tous les {filtered.length} produits filtrés
            </button>
          )}
          <button className="ws-link-sm" onClick={clearSelection} style={{ cursor: 'pointer' }}>Désélectionner</button>
          <div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
            <button className="ws-btn ws-btn-primary" disabled={bulkDeleting} onClick={bulkDelete}
              style={{ background: 'var(--danger)', borderColor: 'var(--danger)' }}>
              <Icon name="trash" size={14}/> {bulkDeleting ? 'Suppression…' : `Supprimer (${selected.size})`}
            </button>
          </div>
        </div>
      )}

      <div className="adm-card">
        <div className="adm-toolbar" style={{ flexWrap: 'wrap', gap: 10 }}>
          <div className="adm-search-inline" style={{ flex: '1 1 220px', minWidth: 200 }}>
            <Icon name="search" size={14}/>
            <input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Rechercher nom, réf, marque..."/>
          </div>
          <div style={{ flex: '1 1 220px', minWidth: 200 }}>
            <LeafCategoryPicker cats={cats} value={filterCat}
              onChange={setFilterCat}
              placeholder="Toutes catégories" allowClear/>
          </div>
          <select value={filterBrand} onChange={e=>setFilterBrand(e.target.value)}
            className="ws-select"
            style={{ minWidth: 160, flex: '0 1 180px' }}>
            <option value="">Toutes marques</option>
            {allBrands.map(b => <option key={b} value={b}>{b}</option>)}
          </select>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, flex: '0 1 240px' }}>
            <input type="number" min="0" step="0.001"
              value={priceMin} onChange={e=>setPriceMin(e.target.value)}
              placeholder="Prix min"
              className="ws-select"
              style={{ width: 100, padding: '8px 10px' }}/>
            <span className="ws-muted" style={{ fontSize: 12 }}>–</span>
            <input type="number" min="0" step="0.001"
              value={priceMax} onChange={e=>setPriceMax(e.target.value)}
              placeholder="Prix max"
              className="ws-select"
              style={{ width: 100, padding: '8px 10px' }}/>
            <span className="ws-muted" style={{ fontSize: 11 }}>TND</span>
          </div>
          {hasAnyFilter && (
            <button className="ws-btn ws-btn-ghost" onClick={resetFilters} style={{ padding: '8px 12px' }}>
              <Icon name="close" size={12}/> Réinitialiser
            </button>
          )}
        </div>
        <table className="ws-table adm-table">
          <thead>
            <tr>
              <th style={{ width: 36 }}>
                <input type="checkbox"
                  checked={allOnPageChecked}
                  ref={el => { if (el) el.indeterminate = !allOnPageChecked && someOnPageChecked; }}
                  onChange={togglePage}
                  title={allOnPageChecked ? 'Tout désélectionner sur cette page' : 'Tout sélectionner sur cette page'}/>
              </th>
              <th>Produit</th><th>Réf</th><th>Catégorie</th><th>Prix</th><th>Disponibilité</th><th>Visibilité</th><th>Statut</th><th></th>
            </tr>
          </thead>
          <tbody>
            {paged.map(p => {
              const cat = cats.find(c => c.slug === p.cat);
              const isSel = selected.has(p.id);
              const onWeb    = p.web_visible    !== false;
              const onMobile = p.mobile_visible !== false;
              return (
                <tr key={p.id} style={isSel ? { background: 'var(--accent-soft)' } : undefined}>
                  <td>
                    <input type="checkbox" checked={isSel} onChange={() => toggleOne(p.id)}/>
                  </td>
                  <td>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                      <div className="adm-alert-thumb">
                        {p.image_url ? <img src={p.image_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}/> : <ProductImg cat={p.cat} id={p.id}/>}
                      </div>
                      <div><div style={{ fontWeight: 600 }}>{p.nom}</div><div className="ws-muted" style={{ fontSize: 11 }}>{p.marque}</div></div>
                    </div>
                  </td>
                  <td className="adm-mono">{p.ref}</td>
                  <td>{cat?.name || '—'}</td>
                  <td>{fmt(p.prix)}</td>
                  <td><StockStatusPill status={p.stockStatus}/></td>
                  <td>
                    <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
                      <span className={`ws-chip ws-chip-${onWeb ? 'default' : 'muted'}`} title="Visible sur le site public">Web {onWeb ? '✓' : '×'}</span>
                      <span className={`ws-chip ws-chip-${onMobile ? 'default' : 'muted'}`} title="Visible dans l'app pro mobile">App {onMobile ? '✓' : '×'}</span>
                    </div>
                  </td>
                  <td><span className={`ws-chip ws-chip-${p.active !== false ? 'default' : 'muted'}`}>{p.active !== false ? 'Actif' : 'Inactif'}</span></td>
                  <td>
                    <div style={{ display: 'flex', gap: 4 }}>
                      <button className="adm-row-btn" title="Modifier" onClick={()=>setEditing(p)}><Icon name="edit" size={14}/></button>
                      <button className="adm-row-btn" title="Supprimer" onClick={()=>setConfirm(p)}><Icon name="trash" size={14}/></button>
                    </div>
                  </td>
                </tr>
              );
            })}
            {!filtered.length && <tr><td colSpan="10" className="ws-muted" style={{ textAlign: 'center', padding: 30 }}>Aucun produit</td></tr>}
          </tbody>
        </table>
        <Pagination page={page} total={filtered.length} onPage={setPage}/>
      </div>
      {confirm && (
        <ConfirmModal
          title="Supprimer le produit ?"
          message={`"${confirm.nom}" sera définitivement supprimé.`}
          danger
          onCancel={()=>setConfirm(null)}
          onConfirm={()=>onDelete(confirm)}
        />
      )}
    </div>
  );
};

const ProductEditor = ({ p, cats, onBack }) => {
  // `gallery` is the source of truth for all images.
  // First item is the "primary" image (shown on cards, hero, etc.)
  const initialGallery = [
    ...(p.image_url ? [p.image_url] : []),
    ...((p.images || []).filter(u => u && u !== p.image_url)),
  ];
  const [form, setForm] = useAs({
    id: p.id || '',
    nom: p.nom || '',
    marque: p.marque || '',
    ref: p.ref || '',
    cat: p.cat || (cats[0]?.id || 'leds'),
    prix: p.prix ?? 0,
    prixOld: p.prixOld ?? '',
    stock: p.stock ?? 0,
    stockStatus: p.stockStatus || 'disponible',
    note: p.note ?? '',
    avis: p.avis ?? 0,
    tag: p.tag || '',
    desc: p.desc || '',
    active: p.active !== false,
    web_visible:    p.web_visible    !== false,
    mobile_visible: p.mobile_visible !== false,
  });
  const [gallery, setGallery] = useAs(initialGallery);
  const [urlInput, setUrlInput] = useAs('');
  const [saving, setSaving] = useAs(false);
  const [uploading, setUploading] = useAs(false);
  const fileRef = useAr(null);

  const up = (k, v) => setForm(f => ({ ...f, [k]: v }));

  const save = async () => {
    if (!form.nom.trim()) return pushToast('Le nom est requis', 'err');
    setSaving(true);
    try {
      // Parse the description ONCE at save time and persist the structured
      // shape. Renderers (web + mobile) read this column directly, no live
      // re-parsing. The parser handles any "Capital + lowercase phrase : "
      // boundary so every product — known spec keys or not — gets cleanly
      // structured automatically.
      const parsed = (form.desc && window.parseDescription)
        ? window.parseDescription(form.desc, form.nom)
        : null;

      const payload = {
        ...form,
        prix: Number(form.prix) || 0,
        prixOld: form.prixOld ? Number(form.prixOld) : undefined,
        stock: Number(form.stock) || 0,
        note: form.note ? Number(form.note) : undefined,
        avis: Number(form.avis) || 0,
        tag: form.tag || null,
        image_url: gallery[0] || null,
        images: gallery.slice(1),  // additional images
        description_parsed: parsed,
      };
      // Make sure the chosen category (and its ancestors) exists as a DB row
      // before inserting/updating — otherwise products.category_id FK fails
      // (23503) whenever the picker offers a category that only lives in the
      // bundled tree or an admin override, not yet persisted to Supabase.
      await ensureCategoryPath(payload.cat, cats);
      if (p.nouveau) {
        await window.WS_DB.createProduct(payload);
        pushToast('Produit créé');
      } else {
        await window.WS_DB.updateProduct(p.id, payload);
        pushToast('Produit mis à jour');
      }
      onBack(true);
    } catch (e) {
      pushToast('Erreur : ' + e.message, 'err');
    } finally { setSaving(false); }
  };

  const onFiles = async (e) => {
    const files = Array.from(e.target.files || []);
    if (!files.length) return;
    setUploading(true);
    try {
      const uploaded = [];
      for (const file of files) {
        const normalized = await normalizeProductImage(file);
        const { url } = await window.WS_DB.uploadProductImage(normalized, { productId: form.id || 'new' });
        uploaded.push(url);
      }
      setGallery(g => [...g, ...uploaded]);
      pushToast(`${uploaded.length} image${uploaded.length > 1 ? 's' : ''} ajoutée${uploaded.length > 1 ? 's' : ''}`);
    } catch (err) {
      pushToast('Upload échoué : ' + err.message, 'err');
    } finally {
      setUploading(false);
      if (fileRef.current) fileRef.current.value = '';
    }
  };

  const addUrl = () => {
    const u = urlInput.trim();
    if (!u) return;
    setGallery(g => [...g, u]);
    setUrlInput('');
  };

  const removeImage = (i) => setGallery(g => g.filter((_, j) => j !== i));
  const makePrimary = (i) => setGallery(g => {
    if (i === 0) return g;
    const copy = [...g];
    const [picked] = copy.splice(i, 1);
    return [picked, ...copy];
  });
  const moveLeft  = (i) => setGallery(g => { if (i === 0) return g; const copy = [...g]; [copy[i-1], copy[i]] = [copy[i], copy[i-1]]; return copy; });
  const moveRight = (i) => setGallery(g => { if (i === g.length - 1) return g; const copy = [...g]; [copy[i+1], copy[i]] = [copy[i], copy[i+1]]; return copy; });

  return (
    <div>
      <button className="ws-link-sm" onClick={()=>onBack(false)} style={{ marginBottom: 12 }}><Icon name="chevL" size={12}/> Retour aux produits</button>
      <div className="adm-page-head">
        <h1>{p.nouveau ? 'Nouveau produit' : form.nom}</h1>
        <div style={{ display: 'flex', gap: 8 }}>
          <button className="ws-btn ws-btn-ghost" onClick={()=>onBack(false)}>Annuler</button>
          <button className="ws-btn ws-btn-primary" disabled={saving} onClick={save}>
            {saving ? 'Enregistrement…' : 'Enregistrer'}
          </button>
        </div>
      </div>
      <div className="adm-editor">
        <div className="adm-card">
          <h3>Informations principales</h3>
          <div className="ws-form-grid">
            <div className="ws-span-2"><label>Nom du produit *</label><input value={form.nom} onChange={e=>up('nom', e.target.value)}/></div>
            <div><label>Marque</label><input value={form.marque} onChange={e=>up('marque', e.target.value)}/></div>
            <div><label>Référence</label><input value={form.ref} onChange={e=>up('ref', e.target.value)}/></div>
            <div>
              <label>Catégorie (feuille)</label>
              <LeafCategoryPicker cats={cats} value={form.cat}
                onChange={(v) => up('cat', v)}
                placeholder="— Choisir —"/>
            </div>
            <div>
              <label>Tag</label>
              <select value={form.tag} onChange={e=>up('tag', e.target.value)}>
                <option value="">Aucun</option>
                <option value="Best-seller">Best-seller</option>
                <option value="Promo">Promo</option>
                <option value="Nouveau">Nouveau</option>
                <option value="Pro">Pro</option>
              </select>
            </div>
            <div className="ws-span-2"><label>Description</label><textarea rows="4" value={form.desc} onChange={e=>up('desc', e.target.value)}/></div>
            <div className="ws-span-2">
              <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                <input type="checkbox" checked={form.active} onChange={e=>up('active', e.target.checked)}/>
                Produit actif (master switch — décoche pour le retirer partout)
              </label>
            </div>
          </div>
        </div>

        <div className="adm-card">
          <h3>Visibilité par plateforme</h3>
          <p className="ws-muted" style={{ marginTop: -4, marginBottom: 12, fontSize: 13 }}>
            Choisissez où ce produit apparaît. Le site web est un catalogue public sans prix ni commande ; l'app mobile Pro affiche les prix et accepte les commandes.
          </p>
          <div className="ws-form-grid">
            <div className="ws-span-2">
              <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                <input type="checkbox" checked={form.web_visible} onChange={e=>up('web_visible', e.target.checked)}/>
                <Icon name="grid" size={14}/> Afficher sur le site web (catalogue public, sans prix)
              </label>
            </div>
            <div className="ws-span-2">
              <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                <input type="checkbox" checked={form.mobile_visible} onChange={e=>up('mobile_visible', e.target.checked)}/>
                <Icon name="phone" size={14}/> Afficher dans l'app Pro (avec prix & commandes)
              </label>
            </div>
          </div>
        </div>

        <div className="adm-card">
          <h3>Prix & Stock</h3>
          <p className="ws-muted" style={{ marginTop: -4, marginBottom: 12, fontSize: 13 }}>
            Les prix ne sont visibles que dans l'application mobile Pro. Le site web public n'affiche jamais les prix.
          </p>
          <div className="ws-form-grid">
            <div><label>Prix TTC (TND)</label><input type="number" step="0.001" value={form.prix} onChange={e=>up('prix', e.target.value)}/></div>
            <div><label>Prix barré (optionnel)</label><input type="number" step="0.001" value={form.prixOld} onChange={e=>up('prixOld', e.target.value)}/></div>
            <div><label>Note (0–5)</label><input type="number" step="0.1" min="0" max="5" value={form.note} onChange={e=>up('note', e.target.value)}/></div>
            <div><label>Nombre d'avis</label><input type="number" value={form.avis} onChange={e=>up('avis', e.target.value)}/></div>
          </div>
          <div style={{ marginTop: 14 }}>
            <label>Disponibilité</label>
            <div className="adm-stock-choice" style={{ marginTop: 6 }}>
              {Object.entries(STOCK_STATUSES).map(([k, s]) => (
                <button key={k} type="button"
                  className={form.stockStatus === k ? 'active' : ''}
                  style={form.stockStatus === k ? { background: s.bg, color: s.color } : undefined}
                  onClick={() => up('stockStatus', k)}>
                  <Icon name={s.icon} size={11}/> {s.label}
                </button>
              ))}
            </div>
          </div>
        </div>

        <div className="adm-card">
          <h3>Galerie d'images ({gallery.length})</h3>
          <div className="ws-muted" style={{ fontSize: 13, marginBottom: 14 }}>
            La première image est l'image principale (vignette). Cliquez sur ⭐ pour la désigner, utilisez les flèches pour réorganiser.
            {' '}
            {window.WS_DB?.isLive?.()
              ? <>Stocké dans Supabase Storage.</>
              : <>Mode démo : images encodées en base64 (max 800 KB chacune).</>}
          </div>

          <div className="adm-gallery-grid">
            {gallery.map((url, i) => (
              <div key={i} className={`adm-gallery-item ${i === 0 ? 'primary' : ''}`}>
                <img src={url} alt=""/>
                {i === 0 && <span className="adm-gallery-badge">Principale</span>}
                <div className="adm-gallery-actions">
                  {i !== 0 && (
                    <button title="Définir comme principale" onClick={()=>makePrimary(i)}>
                      <Icon name="star" size={14}/>
                    </button>
                  )}
                  <button title="Déplacer à gauche" disabled={i === 0} onClick={()=>moveLeft(i)}>
                    <Icon name="chevL" size={14}/>
                  </button>
                  <button title="Déplacer à droite" disabled={i === gallery.length - 1} onClick={()=>moveRight(i)}>
                    <Icon name="chevR" size={14}/>
                  </button>
                  <button title="Supprimer" onClick={()=>removeImage(i)}>
                    <Icon name="trash" size={14}/>
                  </button>
                </div>
              </div>
            ))}
            <button className="adm-gallery-add" disabled={uploading} onClick={()=>fileRef.current?.click()}>
              <Icon name="plus" size={24}/>
              <span>{uploading ? 'Upload…' : 'Ajouter'}</span>
              <span className="ws-muted" style={{ fontSize: 11 }}>une ou plusieurs</span>
            </button>
          </div>

          <input ref={fileRef} type="file" accept="image/*" multiple onChange={onFiles} style={{ display: 'none' }}/>

          <div style={{ marginTop: 16, display: 'flex', gap: 8 }}>
            <input
              value={urlInput}
              onChange={e=>setUrlInput(e.target.value)}
              placeholder="Ou coller une URL d'image…"
              style={{ flex: 1 }}
              onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); addUrl(); } }}
            />
            <button className="ws-btn ws-btn-ghost" onClick={addUrl}>Ajouter URL</button>
          </div>
        </div>
      </div>
    </div>
  );
};

// ==================================================================
//  Categories CRUD
// ==================================================================
// Group categories by parent — returns { [parentId | '_root']: [child, ...] }
// sorted by display_order ascending. Used by the tree renderer + selectors.
const groupByParent = (cats) => {
  const by = {};
  for (const c of cats) {
    const k = c.parent_id || '_root';
    (by[k] = by[k] || []).push(c);
  }
  Object.values(by).forEach(list => list.sort((a, b) => (a.display_order || 0) - (b.display_order || 0)));
  return by;
};

// Searchable leaf-category picker. Closed = button showing selected label;
// open = input + filtered list. Used in the product editor + products filter.
const LeafCategoryPicker = ({ cats, value, onChange, placeholder = '— Choisir —', allowClear = false }) => {
  const [open, setOpen] = useAs(false);
  const [q, setQ] = useAs('');
  const wrapRef = useAr(null);
  const inputRef = useAr(null);

  const leaves = flattenLeaves(cats);
  const selected = leaves.find(l => l.id === value) || null;

  useAe(() => {
    if (open && inputRef.current) inputRef.current.focus();
  }, [open]);

  useAe(() => {
    const close = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) { setOpen(false); setQ(''); } };
    document.addEventListener('mousedown', close);
    return () => document.removeEventListener('mousedown', close);
  }, []);

  const query = q.trim().toLowerCase();
  const filtered = !query ? leaves : leaves.filter(l => l.label.toLowerCase().includes(query));

  const pick = (id) => {
    onChange(id);
    setOpen(false);
    setQ('');
  };

  return (
    <div ref={wrapRef} style={{ position: 'relative' }}>
      <button type="button"
        onClick={() => setOpen(o => !o)}
        style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          gap: 8, width: '100%', padding: '10px 12px', textAlign: 'left',
          background: 'var(--surface)', border: '1px solid var(--line)',
          color: selected ? 'var(--text)' : 'var(--muted)',
          cursor: 'pointer', fontSize: 14, fontFamily: 'inherit',
        }}>
        <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
          {selected ? selected.label : placeholder}
        </span>
        <Icon name={open ? 'chevU' : 'chevD'} size={12}/>
      </button>
      {open && (
        <div style={{
          position: 'absolute', top: 'calc(100% + 4px)', left: 0, right: 0,
          background: 'var(--bg-2)', border: '1px solid var(--line)',
          boxShadow: '0 12px 32px rgba(0,0,0,0.18)', zIndex: 200,
          maxHeight: 360, display: 'flex', flexDirection: 'column',
        }}>
          <div style={{ padding: 8, borderBottom: '1px solid var(--line)', display: 'flex', alignItems: 'center', gap: 6 }}>
            <Icon name="search" size={14} style={{ color: 'var(--muted)' }}/>
            <input ref={inputRef} value={q} onChange={e => setQ(e.target.value)}
              placeholder={`Rechercher parmi ${leaves.length} catégorie${leaves.length > 1 ? 's' : ''}…`}
              style={{ flex: 1, padding: '6px 4px', border: 0, outline: 'none', background: 'transparent', color: 'var(--text)', fontSize: 13 }}/>
            {q && <button type="button" className="adm-row-btn" onClick={() => setQ('')} title="Effacer"><Icon name="close" size={12}/></button>}
          </div>
          <div style={{ overflowY: 'auto', flex: 1 }}>
            {allowClear && (
              <button type="button" onClick={() => pick('')}
                style={{ display: 'block', width: '100%', textAlign: 'left', padding: '8px 12px', background: 'transparent', border: 0, color: 'var(--muted)', cursor: 'pointer', fontSize: 13, fontStyle: 'italic' }}>
                {placeholder}
              </button>
            )}
            {filtered.length === 0 && (
              <div style={{ padding: '12px', color: 'var(--muted)', fontSize: 13, textAlign: 'center' }}>Aucune catégorie</div>
            )}
            {filtered.map(o => (
              <button type="button" key={o.id} onClick={() => pick(o.id)}
                style={{
                  display: 'block', width: '100%', textAlign: 'left',
                  padding: '8px 12px', background: o.id === value ? 'var(--accent-soft)' : 'transparent',
                  border: 0, borderBottom: '1px solid var(--line)',
                  color: o.id === value ? 'var(--accent)' : 'var(--text)',
                  cursor: 'pointer', fontSize: 13, fontFamily: 'inherit',
                }}>
                {o.label}
              </button>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

// Flatten the category list to only leaves (no children), each labelled with
// its full breadcrumb path so admins can pick an unambiguous category for a
// product. Returns [{ id, label, level }, ...].
//
// Accepts either a flat list (legacy: rows from Supabase with parent_id /nom)
// OR a flat list produced by `flattenCategoryTree` (admin merged tree:
// slug / name / parent_slug / has_children). Auto-detects the shape.
const flattenLeaves = (flatOrCats) => {
  if (!flatOrCats?.length) return [];
  const isMergedShape = 'has_children' in flatOrCats[0];
  if (isMergedShape) {
    const byId = Object.fromEntries(flatOrCats.map(c => [c.slug, c]));
    return flatOrCats.filter(c => !c.has_children).map(c => {
      const path = []; let cur = c;
      while (cur) { path.unshift(cur.name); cur = cur.parent_slug ? byId[cur.parent_slug] : null; }
      return { id: c.slug, label: path.join(' › '), level: c.level };
    }).sort((a, b) => a.label.localeCompare(b.label));
  }
  // Legacy Supabase-row shape
  const groups = groupByParent(flatOrCats);
  const byId = Object.fromEntries(flatOrCats.map(c => [c.id, c]));
  return flatOrCats
    .filter(c => !(groups[c.id] || []).length)
    .map(c => {
      const path = []; let cur = c;
      while (cur) { path.unshift(cur.nom); cur = cur.parent_id ? byId[cur.parent_id] : null; }
      return { id: c.id, label: path.join(' › '), level: c.level || 1 };
    })
    .sort((a, b) => a.label.localeCompare(b.label));
};

// Lets the admin pick which L2 sub-categories appear on the public home
// page in the "Rayons" grid. Order is preserved (selection order). Persists
// to the `home_rayons` setting and fires `ws-rayons-changed` so the open
// front-end updates without a reload.
const FeaturedRayonsPanel = () => {
  const { tree } = useMergedCategories();
  const [picked, setPicked] = useAs([]);
  const [saving, setSaving] = useAs(false);
  const [loaded, setLoaded] = useAs(false);

  useAe(() => {
    window.WS_DB.getSetting('home_rayons').then(v => {
      setPicked(Array.isArray(v) ? v : []);
      setLoaded(true);
    }).catch(() => setLoaded(true));
  }, []);

  // Build the L1/L2 view directly from the merged tree.
  const l1s = tree.map(t => ({ id: t.slug, nom: t.name, _children: t.children || [] }));
  const byId = {};
  for (const t of tree) {
    byId[t.slug] = { id: t.slug, nom: t.name };
    for (const c of (t.children || [])) byId[c.slug] = { id: c.slug, nom: c.name };
  }

  const toggle = (id) => {
    const next = picked.includes(id)
      ? picked.filter(x => x !== id)
      : [...picked, id];
    setPicked(next);
  };

  const save = async () => {
    setSaving(true);
    try {
      await window.WS_DB.setSetting('home_rayons', picked);
      try { window.dispatchEvent(new CustomEvent('ws-rayons-changed')); } catch (_) {}
      pushToast(`Rayons enregistrés (${picked.length})`);
    } catch (e) {
      pushToast('Erreur : ' + (e.message || e), 'err');
    } finally { setSaving(false); }
  };

  const reset = () => setPicked([]);

  if (!loaded) return null;

  return (
    <div className="adm-card" style={{ padding: 18, marginBottom: 16 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12, gap: 12, flexWrap: 'wrap' }}>
        <div>
          <h3 style={{ margin: 0 }}>Rayons en vedette · page d'accueil</h3>
          <p className="ws-muted" style={{ margin: '4px 0 0', fontSize: 12 }}>
            Cochez les sous-catégories à mettre en avant. Recommandé : 8 rayons. Ordre = ordre de sélection.
          </p>
        </div>
        <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
          <span className="ws-chip ws-chip-default">{picked.length} sélectionné{picked.length > 1 ? 's' : ''}</span>
          <button className="ws-btn ws-btn-ghost" onClick={reset} disabled={saving || !picked.length}>Tout désélectionner</button>
          <button className="ws-btn ws-btn-primary" onClick={save} disabled={saving}>
            {saving ? 'Enregistrement…' : 'Enregistrer'}
          </button>
        </div>
      </div>

      {picked.length > 0 && (
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 14, padding: '10px 12px', background: 'var(--surface)', border: '1px solid var(--line)' }}>
          <span className="ws-muted" style={{ fontSize: 11, marginRight: 6, alignSelf: 'center' }}>Ordre d'affichage :</span>
          {picked.map((id, i) => (
            <span key={id} className="ws-chip ws-chip-default" style={{ fontSize: 11 }}>
              {i + 1}. {byId[id]?.nom || id}
            </span>
          ))}
        </div>
      )}

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 14 }}>
        {l1s.map(l1 => {
          const l2s = (l1._children || []).map(c => ({ id: c.slug, nom: c.name }));
          if (!l2s.length) return null;
          return (
            <div key={l1.id} style={{ border: '1px solid var(--line)', padding: 12, background: 'var(--surface)' }}>
              <strong style={{ fontSize: 12.5, color: 'var(--accent)', display: 'block', marginBottom: 8 }}>{l1.nom}</strong>
              {l2s.map(l2 => (
                <label key={l2.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0', cursor: 'pointer', fontSize: 13 }}>
                  <input type="checkbox" checked={picked.includes(l2.id)} onChange={() => toggle(l2.id)}/>
                  <span>{l2.nom}</span>
                </label>
              ))}
            </div>
          );
        })}
      </div>
    </div>
  );
};

// Sister panel to FeaturedRayonsPanel — lets the admin pick which L2 categories
// get a dedicated "product preview row" on the home page. Persists to the
// `home_showcases` setting and fires `ws-showcases-changed` so the front-end
// updates without a reload. If empty, the front-end auto-picks the top L2s by
// product count.
const FeaturedShowcasesPanel = () => {
  const { tree } = useMergedCategories();
  const [picked, setPicked] = useAs([]);
  const [saving, setSaving] = useAs(false);
  const [loaded, setLoaded] = useAs(false);

  useAe(() => {
    window.WS_DB.getSetting('home_showcases').then(v => {
      setPicked(Array.isArray(v) ? v : []);
      setLoaded(true);
    }).catch(() => setLoaded(true));
  }, []);

  const l1s = tree.map(t => ({ id: t.slug, nom: t.name, _children: t.children || [] }));
  const byId = {};
  for (const t of tree) {
    byId[t.slug] = { id: t.slug, nom: t.name };
    for (const c of (t.children || [])) byId[c.slug] = { id: c.slug, nom: c.name };
  }

  const toggle = (id) => {
    const next = picked.includes(id)
      ? picked.filter(x => x !== id)
      : [...picked, id];
    setPicked(next);
  };

  const save = async () => {
    setSaving(true);
    try {
      await window.WS_DB.setSetting('home_showcases', picked);
      try { window.dispatchEvent(new CustomEvent('ws-showcases-changed')); } catch (_) {}
      pushToast(`Sélections home enregistrées (${picked.length})`);
    } catch (e) {
      pushToast('Erreur : ' + (e.message || e), 'err');
    } finally { setSaving(false); }
  };

  const reset = () => setPicked([]);
  if (!loaded) return null;

  return (
    <div className="adm-card" style={{ padding: 18, marginBottom: 16 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12, gap: 12, flexWrap: 'wrap' }}>
        <div>
          <h3 style={{ margin: 0 }}>Produits en vedette · page d'accueil</h3>
          <p className="ws-muted" style={{ margin: '4px 0 0', fontSize: 12 }}>
            Chaque sous-catégorie cochée s'affiche en ligne de produits sur la home (4 produits + lien « Voir tout »). Vide = sélection automatique des sous-catégories les mieux fournies.
          </p>
        </div>
        <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
          <span className="ws-chip ws-chip-default">{picked.length} sélectionné{picked.length > 1 ? 's' : ''}</span>
          <button className="ws-btn ws-btn-ghost" onClick={reset} disabled={saving || !picked.length}>Tout désélectionner</button>
          <button className="ws-btn ws-btn-primary" onClick={save} disabled={saving}>
            {saving ? 'Enregistrement…' : 'Enregistrer'}
          </button>
        </div>
      </div>

      {picked.length > 0 && (
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 14, padding: '10px 12px', background: 'var(--surface)', border: '1px solid var(--line)' }}>
          <span className="ws-muted" style={{ fontSize: 11, marginRight: 6, alignSelf: 'center' }}>Ordre d'affichage :</span>
          {picked.map((id, i) => (
            <span key={id} className="ws-chip ws-chip-default" style={{ fontSize: 11 }}>
              {i + 1}. {byId[id]?.nom || id}
            </span>
          ))}
        </div>
      )}

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 14 }}>
        {l1s.map(l1 => {
          const l2s = (l1._children || []).map(c => ({ id: c.slug, nom: c.name }));
          if (!l2s.length) return null;
          return (
            <div key={l1.id} style={{ border: '1px solid var(--line)', padding: 12, background: 'var(--surface)' }}>
              <strong style={{ fontSize: 12.5, color: 'var(--accent)', display: 'block', marginBottom: 8 }}>{l1.nom}</strong>
              {l2s.map(l2 => (
                <label key={l2.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0', cursor: 'pointer', fontSize: 13 }}>
                  <input type="checkbox" checked={picked.includes(l2.id)} onChange={() => toggle(l2.id)}/>
                  <span>{l2.nom}</span>
                </label>
              ))}
            </div>
          );
        })}
      </div>
    </div>
  );
};

// Auto-slugify the name for new categories.
const slugify = (s) => (s || '')
  .normalize('NFD').replace(/[̀-ͯ]/g, '')
  .toLowerCase().trim()
  .replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');

// Build the merged tree: bundled tree + admin overrides (single source of
// truth for the entire site). Returns both the tree and a flat list with
// path / level / parent so the table renderer is straightforward.
const useMergedCategories = () => {
  const [overrides, setOverrides] = useAs(null);
  const refresh = () => window.WS_DB.getSetting('cat_overrides').then(v => setOverrides(v || {}));
  useAe(() => { refresh(); }, []);
  const base = window.WS_CATEGORIES_TREE || [];
  const tree = window.applyCategoryOverrides(base, overrides || {});
  const flat = window.flattenCategoryTree(tree);
  return { tree, flat, overrides: overrides || {}, refresh };
};

// Make sure `slug` (and every ancestor) exists as a row in the Supabase
// categories table before a product is inserted/updated against it.
// Without this, products.category_id FK fails (Postgres 23503) whenever the
// picker offers a category that only exists in the bundled tree or in an
// admin override, not yet as a DB row.
//
// Resolves the ancestor chain from `cats` (the MERGED tree = bundled scrape
// tree + admin overrides, e.g. from useMergedCategories().flat), NOT from the
// raw bundled WS_CATEGORIES_TREE — categories the admin added in the
// dashboard (e.g. a new "Onduleur" leaf) live only in the overrides.
const ensureCategoryPath = async (slug, cats) => {
  if (!slug) return;
  const bySlug = Object.fromEntries((cats || []).map(c => [c.slug, c]));
  const path = [];
  let cur = bySlug[slug];
  while (cur) {
    path.unshift(cur);
    cur = cur.parent_slug ? bySlug[cur.parent_slug] : null;
  }
  // Fallback: category not in the merged tree at all (shouldn't happen from
  // the picker, but be safe) — create the leaf on its own so the FK holds.
  if (!path.length) {
    try { await window.WS_DB.upsertCategory({ id: slug, nom: slug, level: 1, active: true }); }
    catch (e) { console.warn('[ensureCategoryPath] fallback failed for', slug, e.message); }
    return;
  }
  // Upsert parents before children so each parent_id FK is satisfiable.
  for (let i = 0; i < path.length; i++) {
    const node = path[i];
    const parent = i > 0 ? path[i - 1].slug : null;
    try {
      await window.WS_DB.upsertCategory({
        id: node.slug, nom: node.name || node.slug,
        parent_id: parent, level: node.level ?? (i + 1), active: true,
      });
    } catch (e) {
      console.warn('[ensureCategoryPath] failed for', node.slug, e.message);
    }
  }
};

const AdminCats = () => {
  const { tree, flat, overrides, refresh } = useMergedCategories();
  const [products, setProducts] = useAs([]);
  const [editing, setEditing] = useAs(null);
  const [confirm, setConfirm] = useAs(null);
  const [expanded, setExpanded] = useAs(() => new Set());
  const [selected, setSelected] = useAs(() => new Set());

  useAe(() => {
    window.WS_DB.listProducts({ activeOnly: false }).then(setProducts).catch(() => {});
  }, []);

  // Persist a partial patch onto the override for `slug`. Pass `_deleted: true`
  // to hide a node, or a fresh override with `_added: true` to create one.
  const saveOverride = async (slug, patch) => {
    const next = { ...(overrides || {}) };
    next[slug] = { ...(next[slug] || {}), ...patch };
    // Clean empty overrides (back to bundled defaults)
    const ov = next[slug];
    const isEmpty = !ov._added && !ov._deleted
      && (ov.nom == null || ov.nom === '')
      && ov.desc == null
      && ov.display_order == null
      && (ov.active === true || ov.active === undefined);
    if (isEmpty) delete next[slug];
    await window.WS_DB.setSetting('cat_overrides', next);
    try { window.dispatchEvent(new CustomEvent('ws-cats-changed')); } catch (_) {}
    refresh();
  };

  const onDelete = async (slug) => {
    const ov = overrides[slug];
    // If it was an admin-created node, drop the override entirely.
    if (ov?._added) {
      const next = { ...overrides };
      delete next[slug];
      await window.WS_DB.setSetting('cat_overrides', next);
      try { window.dispatchEvent(new CustomEvent('ws-cats-changed')); } catch (_) {}
      refresh();
    } else {
      // Otherwise mark as deleted (hides on client).
      await saveOverride(slug, { _deleted: true });
    }
    pushToast('Catégorie supprimée');
    setConfirm(null);
  };

  // ----- Bulk selection -----
  const toggleSel = (slug) => {
    const n = new Set(selected);
    n.has(slug) ? n.delete(slug) : n.add(slug);
    setSelected(n);
  };
  const clearSel = () => setSelected(new Set());

  // Walk a slug + all its descendants. Used by bulk-delete to also remove
  // children and by visible-row helpers below.
  const descendantsOf = (slug) => {
    const out = [];
    const walk = (s) => {
      const kids = flat.filter(c => c.parent_slug === s);
      for (const k of kids) { out.push(k.slug); walk(k.slug); }
    };
    walk(slug);
    return out;
  };

  const bulkDelete = async () => {
    if (!selected.size) return;
    // Expand selection to include descendants so the tree stays consistent.
    const all = new Set(selected);
    for (const s of selected) descendantsOf(s).forEach(d => all.add(d));
    const n = all.size;
    if (!window.confirm(`Supprimer ${n} catégorie${n > 1 ? 's' : ''} (descendants inclus) ? Les produits associés conserveront leur référence catégorie mais ne s'afficheront plus côté client.`)) return;

    const next = { ...(overrides || {}) };
    for (const slug of all) {
      if (next[slug]?._added) {
        // Admin-added node — drop the override entirely.
        delete next[slug];
      } else {
        next[slug] = { ...(next[slug] || {}), _deleted: true };
      }
    }
    await window.WS_DB.setSetting('cat_overrides', next);
    try { window.dispatchEvent(new CustomEvent('ws-cats-changed')); } catch (_) {}
    refresh();
    setSelected(new Set());
    pushToast(`${n} catégorie${n > 1 ? 's' : ''} supprimée${n > 1 ? 's' : ''}`);
  };

  const counts = {
    l1: flat.filter(c => c.level === 1).length,
    l2: flat.filter(c => c.level === 2).length,
    l3: flat.filter(c => c.level === 3).length,
  };

  const childrenOf = (slug) => flat.filter(c => c.parent_slug === slug);

  const toggle = (id) => {
    const n = new Set(expanded);
    n.has(id) ? n.delete(id) : n.add(id);
    setExpanded(n);
  };
  const expandAll = () => setExpanded(new Set(flat.map(c => c.slug)));
  const collapseAll = () => setExpanded(new Set());

  // Recursively render a node + its children. Indentation by level.
  const renderRow = (c) => {
    const kids = childrenOf(c.slug);
    const hasChildren = kids.length > 0;
    const isOpen = expanded.has(c.slug);
    const productCount = products.filter(p => p.cat === c.slug).length;
    const isCustom = overrides[c.slug]?._added === true;
    const isEdited = !!overrides[c.slug] && !overrides[c.slug]?._deleted;
    const isSel = selected.has(c.slug);
    return (
      <React.Fragment key={c.slug}>
        <tr style={isSel ? { background: 'var(--accent-soft)' } : undefined}>
          <td>
            <input type="checkbox" checked={isSel} onChange={() => toggleSel(c.slug)}/>
          </td>
          <td style={{ paddingLeft: 14 + (c.level - 1) * 24 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              {hasChildren ? (
                <button className="adm-row-btn" onClick={() => toggle(c.slug)} title={isOpen ? 'Replier' : 'Déplier'}>
                  <Icon name={isOpen ? 'chevD' : 'chevR'} size={12}/>
                </button>
              ) : (
                <span style={{ width: 22, display: 'inline-block' }}/>
              )}
              <strong style={{ fontSize: c.level === 1 ? 14 : 13, fontWeight: c.level === 1 ? 700 : 500 }}>{c.name}</strong>
              {isCustom && <span className="ws-chip ws-chip-pro" style={{ fontSize: 10 }}>Ajoutée</span>}
              {isEdited && !isCustom && <span className="ws-chip ws-chip-default" style={{ fontSize: 10 }}>Modifiée</span>}
            </div>
          </td>
          <td><code style={{ fontSize: 11, color: 'var(--muted)' }}>{c.slug}</code></td>
          <td>L{c.level}</td>
          <td>{hasChildren ? `${kids.length} enfant${kids.length > 1 ? 's' : ''}` : '—'}</td>
          <td>{productCount > 0 ? <strong>{productCount}</strong> : <span className="ws-muted">0</span>}</td>
          <td>{overrides[c.slug]?.display_order ?? '—'}</td>
          <td>
            <div style={{ display: 'flex', gap: 4 }}>
              {c.level < 3 && (
                <button className="adm-row-btn" title="Ajouter une sous-catégorie"
                  onClick={() => setEditing({
                    nouveau: true, slug: '', nom: '', desc: '',
                    parent_slug: c.slug, level: c.level + 1,
                    display_order: (kids.length + 1) * 10, active: true,
                  })}>
                  <Icon name="plus" size={12}/>
                </button>
              )}
              <button className="adm-row-btn" title="Modifier" onClick={() => setEditing({
                slug: c.slug, nom: c.name, desc: overrides[c.slug]?.desc || '',
                parent_slug: c.parent_slug, level: c.level,
                display_order: overrides[c.slug]?.display_order ?? 0,
                active: overrides[c.slug]?.active !== false,
              })}><Icon name="edit" size={12}/></button>
              <button className="adm-row-btn" title="Supprimer" onClick={() => setConfirm(c)}><Icon name="trash" size={12}/></button>
            </div>
          </td>
        </tr>
        {isOpen && kids.map(renderRow)}
      </React.Fragment>
    );
  };

  const tops = flat.filter(c => c.level === 1);
  const editedCount = Object.values(overrides).filter(v => !v._added && !v._deleted).length;
  const addedCount  = Object.values(overrides).filter(v => v._added).length;
  const deletedCount = Object.values(overrides).filter(v => v._deleted).length;

  const resetAll = async () => {
    if (!window.confirm('Réinitialiser toutes les modifications du catalogue ? Toutes les catégories reviendront à leur état d\'origine.')) return;
    await window.WS_DB.setSetting('cat_overrides', {});
    try { window.dispatchEvent(new CustomEvent('ws-cats-changed')); } catch (_) {}
    refresh();
    pushToast('Catalogue réinitialisé');
  };

  return (
    <div>
      <div className="adm-page-head">
        <div>
          <h1>Catégories</h1>
          <p className="ws-muted">{flat.length} entrées · {counts.l1} L1 · {counts.l2} L2 · {counts.l3} L3 · {products.length} produits</p>
          {(editedCount + addedCount + deletedCount > 0) && (
            <p className="ws-muted" style={{ fontSize: 12, marginTop: 4 }}>
              {editedCount > 0 && <>{editedCount} modifiée{editedCount > 1 ? 's' : ''} · </>}
              {addedCount > 0 && <>{addedCount} ajoutée{addedCount > 1 ? 's' : ''} · </>}
              {deletedCount > 0 && <>{deletedCount} masquée{deletedCount > 1 ? 's' : ''}</>}
            </p>
          )}
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          {(editedCount + addedCount + deletedCount > 0) && (
            <button className="ws-btn ws-btn-ghost" onClick={resetAll}>Réinitialiser</button>
          )}
          <button className="ws-btn ws-btn-ghost" onClick={expandAll}>Tout déplier</button>
          <button className="ws-btn ws-btn-ghost" onClick={collapseAll}>Tout replier</button>
          <button className="ws-btn ws-btn-primary"
            onClick={() => setEditing({
              nouveau: true, slug: '', nom: '', desc: '',
              parent_slug: null, level: 1,
              display_order: (tops.length + 1) * 10, active: true,
            })}>
            <Icon name="plus" size={14}/> Nouvelle catégorie L1
          </button>
        </div>
      </div>

      <FeaturedRayonsPanel/>
      <FeaturedShowcasesPanel/>

      {selected.size > 0 && (
        <div className="adm-card" style={{ padding: 12, marginBottom: 12, display: 'flex', alignItems: 'center', gap: 12, background: 'var(--accent-soft)', border: '1px solid var(--accent)' }}>
          <strong style={{ color: 'var(--accent)' }}>{selected.size} catégorie{selected.size > 1 ? 's' : ''} sélectionnée{selected.size > 1 ? 's' : ''}</strong>
          <button className="ws-link-sm" onClick={clearSel} style={{ cursor: 'pointer' }}>Désélectionner</button>
          <div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
            <button className="ws-btn ws-btn-primary" onClick={bulkDelete}
              style={{ background: 'var(--danger)', borderColor: 'var(--danger)' }}>
              <Icon name="trash" size={14}/> Supprimer ({selected.size})
            </button>
          </div>
        </div>
      )}

      <div className="adm-card">
        <table className="ws-table adm-table">
          <thead>
            <tr>
              <th style={{ width: 36 }}>
                <input type="checkbox"
                  checked={tops.length > 0 && tops.every(c => selected.has(c.slug))}
                  ref={el => { if (el) el.indeterminate = selected.size > 0 && !tops.every(c => selected.has(c.slug)); }}
                  onChange={() => {
                    const allTop = tops.every(c => selected.has(c.slug));
                    const n = new Set(selected);
                    if (allTop) tops.forEach(c => n.delete(c.slug));
                    else tops.forEach(c => n.add(c.slug));
                    setSelected(n);
                  }}
                  title="Tout sélectionner / désélectionner (L1)"/>
              </th>
              <th style={{ paddingLeft: 14 }}>Nom</th>
              <th>Slug</th>
              <th>Niveau</th>
              <th>Enfants</th>
              <th>Produits</th>
              <th>Ordre</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            {tops.map(renderRow)}
            {!flat.length && (
              <tr><td colSpan="8" style={{ textAlign: 'center', padding: 30 }} className="ws-muted">Aucune catégorie</td></tr>
            )}
          </tbody>
        </table>
      </div>

      {editing && <CategoryEditor c={editing} flat={flat}
        onSave={async (data) => {
          // Build the override patch for this slug.
          if (editing.nouveau) {
            const slug = data.slug || slugify(data.nom);
            if (flat.some(c => c.slug === slug)) {
              pushToast(`Le slug « ${slug} » est déjà utilisé par une autre catégorie — choisissez un nom différent`, 'err');
              return;
            }
            await saveOverride(slug, {
              _added: true,
              nom: data.nom,
              desc: data.desc || null,
              parent_id: data.parent_slug || null,
              level: data.level,
              display_order: Number(data.display_order) || 0,
              active: data.active,
            });
            // Auto-expand every ancestor so the freshly created row is
            // immediately visible instead of hiding behind a collapsed
            // parent — otherwise a new L3 looks like it silently failed.
            setExpanded(prev => {
              const next = new Set(prev);
              let cur = data.parent_slug ? flat.find(c => c.slug === data.parent_slug) : null;
              while (cur) {
                next.add(cur.slug);
                cur = cur.parent_slug ? flat.find(c => c.slug === cur.parent_slug) : null;
              }
              return next;
            });
            pushToast('Catégorie créée');
          } else {
            await saveOverride(editing.slug, {
              nom: data.nom !== editing.nom ? data.nom : undefined,
              desc: data.desc || null,
              display_order: Number(data.display_order) || 0,
              active: data.active,
            });
            pushToast('Catégorie mise à jour');
          }
          setEditing(null);
        }}
        onClose={() => setEditing(null)}/>}
      {confirm && (
        <ConfirmModal
          title="Supprimer la catégorie ?"
          message={`"${confirm.name}" sera masquée du site${childrenOf(confirm.slug).length ? ' avec toutes ses sous-catégories' : ''}. Vous pouvez la rétablir en cliquant "Réinitialiser".`}
          danger
          onCancel={() => setConfirm(null)}
          onConfirm={() => onDelete(confirm.slug)}
        />
      )}
    </div>
  );
};

const CategoryEditor = ({ c, flat, onSave, onClose }) => {
  const [form, setForm] = useAs({
    slug: c.slug || '',
    nom: c.nom || '',
    desc: c.desc || '',
    parent_slug: c.parent_slug || null,
    level: c.level || 1,
    display_order: c.display_order ?? 0,
    active: c.active !== false,
  });
  const [saving, setSaving] = useAs(false);
  const up = (k, v) => setForm(f => ({ ...f, [k]: v }));

  // Auto-fill slug from name in create mode
  useAe(() => {
    if (c.nouveau && !form.slug) up('slug', slugify(form.nom));
  }, [form.nom]);

  // When parent changes, auto-derive level
  useAe(() => {
    if (!c.nouveau) return;
    if (!form.parent_slug) up('level', 1);
    else {
      const p = (flat || []).find(x => x.slug === form.parent_slug);
      up('level', (p?.level || 1) + 1);
    }
  }, [form.parent_slug]);

  const submit = async () => {
    if (!form.nom.trim()) return pushToast('Le nom est requis', 'err');
    if (c.nouveau && !form.slug.trim()) return pushToast('Le slug est requis', 'err');
    setSaving(true);
    try { await onSave(form); }
    catch (e) { pushToast('Erreur : ' + (e.message || e), 'err'); }
    finally { setSaving(false); }
  };

  // Eligible parents: any L1 or L2 except the node being edited.
  const parentOpts = (flat || [])
    .filter(x => x.level < 3 && x.slug !== c.slug)
    .sort((a, b) => a.level !== b.level ? a.level - b.level : a.name.localeCompare(b.name));

  return (
    <div className="ws-modal-back" onClick={onClose}>
      <div className="ws-modal" onClick={e => e.stopPropagation()} style={{ maxWidth: 560 }}>
        <h3 style={{ margin: 0 }}>{c.nouveau ? 'Nouvelle catégorie' : 'Modifier la catégorie'}</h3>
        <div className="ws-form-grid" style={{ marginTop: 16 }}>
          <div className="ws-span-2"><label>Nom *</label><input value={form.nom} onChange={e => up('nom', e.target.value)}/></div>
          <div className="ws-span-2">
            <label>Catégorie parente {!c.nouveau && '(non modifiable)'}</label>
            <select value={form.parent_slug || ''} onChange={e => up('parent_slug', e.target.value || null)} disabled={!c.nouveau}>
              <option value="">— Racine (L1) —</option>
              {parentOpts.map(p => (
                <option key={p.slug} value={p.slug}>
                  {'  '.repeat(p.level - 1)}{p.name} (L{p.level})
                </option>
              ))}
            </select>
            <div className="ws-muted" style={{ fontSize: 11, marginTop: 4 }}>Niveau : <strong>L{form.level}</strong></div>
          </div>
          <div className="ws-span-2">
            <label>Slug {c.nouveau ? '*' : '(non modifiable)'}</label>
            <input value={form.slug} disabled={!c.nouveau}
              onChange={e => up('slug', e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))}
              placeholder="ex: relais-de-protection"/>
          </div>
          <div className="ws-span-2"><label>Description</label><textarea rows="2" value={form.desc} onChange={e => up('desc', e.target.value)}/></div>
          <div className="ws-span-2"><label>Ordre d'affichage</label><input type="number" value={form.display_order} onChange={e => up('display_order', Number(e.target.value))}/></div>
          <div className="ws-span-2">
            <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <input type="checkbox" checked={form.active} onChange={e => up('active', e.target.checked)}/>
              Active (visible côté client)
            </label>
          </div>
        </div>
        <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 18 }}>
          <button className="ws-btn ws-btn-ghost" onClick={onClose}>Annuler</button>
          <button className="ws-btn ws-btn-primary" disabled={saving} onClick={submit}>{saving ? 'Enregistrement…' : 'Enregistrer'}</button>
        </div>
      </div>
    </div>
  );
};

// ==================================================================
//  Customers (from profiles)
// ==================================================================
const AdminCustomers = () => {
  const [customers, setCustomers] = useAs([]);
  const [search, setSearch] = useAs('');
  const [loading, setLoading] = useAs(true);
  const [page, setPage] = useAs(1);
  useAe(() => { setPage(1); }, [search]);

  useAe(() => {
    window.WS_DB.listCustomers()
      .then(setCustomers)
      .catch(() => {})
      .finally(() => setLoading(false));
  }, []);

  const list = customers.filter(c => {
    if (!search) return true;
    const q = search.toLowerCase();
    return (c.nom || '').toLowerCase().includes(q)
        || (c.prenom || '').toLowerCase().includes(q)
        || (c.email || '').toLowerCase().includes(q)
        || (c.tel || '').toLowerCase().includes(q);
  });
  const paged = list.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);

  const totalSpent = customers.reduce((a, c) => a + (c.depense || 0), 0);
  const adminCount = customers.filter(c => c.role === 'admin').length;

  return (
    <div>
      <div className="adm-page-head">
        <div><h1>Clients</h1><p className="ws-muted">{customers.length} profils · {window.WS_DB.mode()==='live' ? 'depuis Supabase' : 'mode démo'}</p></div>
      </div>
      <div className="adm-kpis">
        <div className="adm-kpi"><div className="adm-kpi-top"><div className="adm-kpi-icon"><Icon name="users" size={18}/></div></div><div className="adm-kpi-value">{customers.length}</div><div className="adm-kpi-label">Total clients</div></div>
        <div className="adm-kpi"><div className="adm-kpi-top"><div className="adm-kpi-icon"><Icon name="trend" size={18}/></div></div><div className="adm-kpi-value">{customers.filter(c => {
          if (!c.created_at) return false;
          const d = new Date(c.created_at);
          const now = new Date();
          return d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear();
        }).length}</div><div className="adm-kpi-label">Ce mois-ci</div></div>
        <div className="adm-kpi"><div className="adm-kpi-top"><div className="adm-kpi-icon"><Icon name="star" size={18}/></div></div><div className="adm-kpi-value">{adminCount}</div><div className="adm-kpi-label">Admins</div></div>
        <div className="adm-kpi"><div className="adm-kpi-top"><div className="adm-kpi-icon"><Icon name="cart" size={18}/></div></div><div className="adm-kpi-value">{fmt(totalSpent)}</div><div className="adm-kpi-label">CA total TND</div></div>
      </div>
      <div className="adm-card">
        <div className="adm-toolbar">
          <div className="adm-search-inline">
            <Icon name="search" size={14}/>
            <input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Chercher nom, email, téléphone..."/>
          </div>
        </div>
        <table className="ws-table adm-table">
          <thead><tr><th>Client</th><th>Email</th><th>Téléphone</th><th>Commandes</th><th>Dépense</th><th>Rôle</th><th>Inscrit le</th></tr></thead>
          <tbody>
            {paged.map(c => (
              <tr key={c.id}>
                <td>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                    <div className="ws-avatar" style={{ width: 32, height: 32, fontSize: 12 }}>
                      {((c.prenom || '')[0] || '?') + ((c.nom || '')[0] || '')}
                    </div>
                    <div><div style={{ fontWeight: 600 }}>{`${c.prenom || ''} ${c.nom || ''}`.trim() || '—'}</div></div>
                  </div>
                </td>
                <td>{c.email || '—'}</td>
                <td className="adm-mono">{c.tel || '—'}</td>
                <td><strong>{c.commandes || 0}</strong></td>
                <td><strong>{fmt(c.depense || 0)} TND</strong></td>
                <td><span className={`ws-chip ws-chip-${c.role === 'admin' ? 'pro' : 'default'}`}>{c.role === 'admin' ? 'Admin' : 'Client'}</span></td>
                <td className="ws-muted">{c.created_at ? new Date(c.created_at).toLocaleDateString('fr-FR') : '—'}</td>
              </tr>
            ))}
            {!loading && !list.length && <tr><td colSpan="7" className="ws-muted" style={{ textAlign: 'center', padding: 30 }}>Aucun client</td></tr>}
          </tbody>
        </table>
        <Pagination page={page} total={list.length} onPage={setPage}/>
      </div>
    </div>
  );
};

// ==================================================================
//  Reports (fully live — computed from orders + products)
// ==================================================================
const AdminReports = () => {
  const [orders, setOrders]     = useAs([]);
  const [products, setProducts] = useAs([]);
  const [cats, setCats]         = useAs([]);
  const [customers, setCustomers] = useAs([]);

  useAe(() => {
    Promise.all([
      window.WS_DB.listOrders(),
      window.WS_DB.listProducts({ activeOnly: false }),
      window.WS_DB.listCategories(),
      window.WS_DB.listCustomers().catch(() => []),
    ]).then(([o, p, c, cu]) => {
      setOrders(o || []); setProducts(p || []); setCats(c || []); setCustomers(cu || []);
    }).catch(() => {});
  }, []);

  const now = new Date();
  const thisMonth = (d) => {
    const x = new Date(d);
    return x.getMonth() === now.getMonth() && x.getFullYear() === now.getFullYear();
  };
  const lastMonth = (d) => {
    const x = new Date(d);
    const lm = new Date(now); lm.setMonth(lm.getMonth() - 1);
    return x.getMonth() === lm.getMonth() && x.getFullYear() === lm.getFullYear();
  };

  const monthOrders  = orders.filter(o => o.date && thisMonth(o.date));
  const prevOrders   = orders.filter(o => o.date && lastMonth(o.date));
  const monthRevenue = monthOrders.reduce((a, o) => a + Number(o.total || 0) + Number(o.livraison || 0), 0);
  const prevRevenue  = prevOrders.reduce((a, o) => a + Number(o.total || 0) + Number(o.livraison || 0), 0);
  const revenueChange = prevRevenue ? Math.round(((monthRevenue - prevRevenue) / prevRevenue) * 100) : 0;
  const ordersChange  = prevOrders.length ? Math.round(((monthOrders.length - prevOrders.length) / prevOrders.length) * 100) : 0;
  const avgBasket = monthOrders.length ? monthRevenue / monthOrders.length : 0;

  // 12-month revenue series
  const series = Array.from({ length: 12 }, (_, i) => {
    const d = new Date(now); d.setMonth(d.getMonth() - (11 - i));
    const sum = orders
      .filter(o => o.date && new Date(o.date).getMonth() === d.getMonth() && new Date(o.date).getFullYear() === d.getFullYear())
      .reduce((a, o) => a + Number(o.total || 0) + Number(o.livraison || 0), 0);
    return sum;
  });
  const seriesMax = Math.max(1, ...series);

  // category distribution from real products
  const palette = ['#E8652A','#1E3A5F','#F4A261','#4A6B8A','#E8B27A','#B8C5D1','#7A4AE8','#159F6A'];
  const catData = cats.map((c, i) => {
    const count = products.filter(p => p.cat === c.id).length;
    return { nom: c.nom, count, color: palette[i % palette.length] };
  }).sort((a, b) => b.count - a.count);
  const catTotal = catData.reduce((a, c) => a + c.count, 0) || 1;

  const delta = (n) => n === 0 ? 'stable' : (n > 0 ? '+' : '') + n + '%';

  return (
    <div>
      <div className="adm-page-head">
        <div><h1>Rapports & Analyses</h1><p className="ws-muted">{now.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })}</p></div>
      </div>
      <div className="adm-kpis">
        <div className="adm-kpi"><div className="adm-kpi-top"><div className="adm-kpi-icon"><Icon name="chart" size={18}/></div><span className={`adm-delta ${revenueChange>=0?'up':'down'}`}><Icon name={revenueChange>=0?'chevU':'chevD'} size={10}/> {delta(revenueChange)}</span></div><div className="adm-kpi-value">{fmt(monthRevenue)} TND</div><div className="adm-kpi-label">CA du mois</div></div>
        <div className="adm-kpi"><div className="adm-kpi-top"><div className="adm-kpi-icon"><Icon name="box" size={18}/></div><span className={`adm-delta ${ordersChange>=0?'up':'down'}`}><Icon name={ordersChange>=0?'chevU':'chevD'} size={10}/> {delta(ordersChange)}</span></div><div className="adm-kpi-value">{monthOrders.length}</div><div className="adm-kpi-label">Commandes ce mois</div></div>
        <div className="adm-kpi"><div className="adm-kpi-top"><div className="adm-kpi-icon"><Icon name="users" size={18}/></div></div><div className="adm-kpi-value">{customers.length}</div><div className="adm-kpi-label">Clients inscrits</div></div>
        <div className="adm-kpi"><div className="adm-kpi-top"><div className="adm-kpi-icon"><Icon name="cart" size={18}/></div></div><div className="adm-kpi-value">{fmt(avgBasket)} TND</div><div className="adm-kpi-label">Panier moyen</div></div>
      </div>
      <div className="adm-row">
        <div className="adm-card adm-chart-card">
          <div className="adm-card-head"><h3>Évolution mensuelle</h3><span className="ws-muted">12 derniers mois</span></div>
          <BarChart data={series} max={seriesMax}/>
        </div>
        <div className="adm-card">
          <div className="adm-card-head"><h3>Top catégories</h3></div>
          {catData.length === 0 && <p className="ws-muted">Aucune donnée.</p>}
          {catData.map(c => {
            const pct = Math.round((c.count / catTotal) * 100);
            return (
              <div key={c.nom} style={{ marginBottom: 14 }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13, marginBottom: 4 }}>
                  <span>{c.nom}</span><strong>{c.count} ({pct}%)</strong>
                </div>
                <div className="adm-stock-bar"><div className="adm-stock-fill" style={{ width: pct+'%', background: c.color }}/></div>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
};

// ==================================================================
//  AdminBrands — manage logo + description for each brand
// ==================================================================
//  Brands are derived from product.marque values (no separate table).
//  Per-brand logo / description live in the `brand_meta` setting:
//    { "Schneider": { logo: "data:image/png;...", description: "..." } }
//  The client Nos marques page reads the same setting.
const AdminBrands = ({ onNav }) => {
  const [products, setProducts] = useAs([]);
  const [meta, setMeta] = useAs({});
  const [editing, setEditing] = useAs(null);
  const [creating, setCreating] = useAs(false);
  const [confirmDelete, setConfirmDelete] = useAs(null);
  const [search, setSearch] = useAs('');
  const [loaded, setLoaded] = useAs(false);

  const load = () => Promise.all([
    window.WS_DB.listProducts({ activeOnly: false }).catch(() => []),
    window.WS_DB.getSetting('brand_meta').catch(() => ({})),
  ]).then(([p, m]) => {
    setProducts(p || []);
    setMeta(m && typeof m === 'object' ? m : {});
    setLoaded(true);
  });

  useAe(() => { load(); }, []);

  // Aggregate brand info from products + merge meta entries (so admin can
  // pre-create a brand even before any product is tagged with it).
  const brands = (() => {
    const m = {};
    for (const p of products) {
      const b = (p.marque || '').trim();
      if (!b) continue;
      if (!m[b]) m[b] = { name: b, count: 0, products: [] };
      m[b].count++;
      m[b].products.push(p);
    }
    for (const name of Object.keys(meta || {})) {
      if (!m[name]) m[name] = { name, count: 0, products: [] };
    }
    return Object.values(m).sort((a, b) => a.name.localeCompare(b.name));
  })();

  const filtered = brands.filter(b => !search || b.name.toLowerCase().includes(search.toLowerCase()));

  const saveMeta = async (name, patch) => {
    const next = { ...(meta || {}) };
    next[name] = { ...(next[name] || {}), ...patch };
    setMeta(next);
    await window.WS_DB.setSetting('brand_meta', next);
    try { window.dispatchEvent(new CustomEvent('ws-brands-changed')); } catch (_) {}
  };

  const removeMeta = async (name) => {
    const next = { ...(meta || {}) };
    delete next[name];
    setMeta(next);
    await window.WS_DB.setSetting('brand_meta', next);
    try { window.dispatchEvent(new CustomEvent('ws-brands-changed')); } catch (_) {}
  };

  // Full brand deletion: drops the meta entry AND clears the marque field
  // on every product carrying this brand. Use with care — irreversible.
  const deleteBrand = async (b) => {
    let updated = 0;
    for (const p of (b.products || [])) {
      try { await window.WS_DB.updateProduct(p.id, { marque: '' }); updated++; }
      catch (_) {}
    }
    await removeMeta(b.name);
    await load();
    pushToast(`Marque supprimée${updated > 0 ? ` · ${updated} produit${updated > 1 ? 's' : ''} mis à jour` : ''}`);
  };

  return (
    <div>
      <div className="adm-page-head">
        <div>
          <h1>Marques</h1>
          <p className="ws-muted">{brands.length} marques · logo + description gérés ici</p>
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          <button className="ws-btn ws-btn-ghost" onClick={load}><Icon name="refresh" size={14}/> Actualiser</button>
          <button className="ws-btn ws-btn-primary" onClick={() => setCreating(true)}>
            <Icon name="plus" size={14}/> Nouvelle marque
          </button>
        </div>
      </div>

      <div className="adm-card" style={{ padding: 12, marginBottom: 12 }}>
        <div className="adm-search-inline">
          <Icon name="search" size={14}/>
          <input value={search} onChange={e => setSearch(e.target.value)} placeholder="Rechercher une marque…"/>
        </div>
      </div>

      {loaded && !brands.length && (
        <div className="adm-card" style={{ padding: 30, textAlign: 'center' }}>
          <p className="ws-muted">Aucune marque encore. Importez ou créez des produits — les marques apparaîtront ici automatiquement.</p>
        </div>
      )}

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 14 }}>
        {filtered.map(b => {
          const m = meta[b.name] || {};
          return (
            <div key={b.name} className="adm-card" style={{ padding: 16 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 10 }}>
                <div style={{ width: 64, height: 64, background: 'var(--surface)', border: '1px solid var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
                  {m.logo
                    ? <img src={m.logo} alt={b.name} style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }}/>
                    : <Icon name="tag" size={22} style={{ color: 'var(--muted)' }}/>}
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <strong style={{ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis' }}>{b.name}</strong>
                  {b.count > 0 ? (
                    <a
                      onClick={() => onNav && onNav('admin-products', { brand: b.name })}
                      style={{ cursor: 'pointer', color: 'var(--accent)', fontSize: 12, fontWeight: 600, textDecoration: 'underline', textDecorationStyle: 'dotted' }}
                      title={`Voir les ${b.count} produits de cette marque`}
                    >
                      {b.count} produit{b.count > 1 ? 's' : ''} →
                    </a>
                  ) : (
                    <div className="ws-muted" style={{ fontSize: 12 }}>0 produit</div>
                  )}
                </div>
              </div>
              {m.description && (
                <p style={{ margin: '0 0 10px', fontSize: 12, color: 'var(--muted)', maxHeight: 60, overflow: 'hidden' }}>{m.description}</p>
              )}
              <div style={{ display: 'flex', gap: 6 }}>
                <button className="ws-btn ws-btn-ghost" style={{ flex: 1 }} onClick={() => setEditing(b)}>
                  <Icon name="edit" size={12}/> {m.logo || m.description ? 'Modifier' : 'Configurer'}
                </button>
                <button className="adm-row-btn" title="Supprimer la marque"
                  style={{ color: 'var(--danger)' }}
                  onClick={() => setConfirmDelete(b)}>
                  <Icon name="trash" size={12}/>
                </button>
              </div>
            </div>
          );
        })}
      </div>

      {editing && (
        <BrandMetaEditor brand={editing} initial={meta[editing.name] || {}}
          existingNames={brands.map(b => b.name)}
          onRename={async (oldName, newName) => {
            // Rename: move the meta entry under the new key AND update every
            // product carrying the old marque to the new one. Cascading
            // updates keep the brand list consistent.
            const next = { ...(meta || {}) };
            if (next[oldName]) {
              next[newName] = { ...next[oldName] };
              delete next[oldName];
            }
            setMeta(next);
            await window.WS_DB.setSetting('brand_meta', next);
            const toUpdate = products.filter(p => (p.marque || '').trim() === oldName);
            for (const p of toUpdate) {
              try { await window.WS_DB.updateProduct(p.id, { marque: newName }); } catch (_) {}
            }
            try { window.dispatchEvent(new CustomEvent('ws-brands-changed')); } catch (_) {}
            await load();
          }}
          onSave={async (patch) => { await saveMeta(editing.name, patch); setEditing(null); pushToast('Marque mise à jour'); }}
          onClose={() => setEditing(null)}/>
      )}
      {creating && (
        <BrandMetaEditor brand={null} initial={{}}
          existingNames={brands.map(b => b.name)}
          onSave={async (patch, name) => {
            const trimmed = (name || '').trim();
            if (!trimmed) { pushToast('Le nom est requis', 'err'); return; }
            if (brands.some(b => b.name.toLowerCase() === trimmed.toLowerCase())) {
              pushToast('Cette marque existe déjà', 'err'); return;
            }
            await saveMeta(trimmed, patch);
            setCreating(false);
            pushToast('Marque créée');
          }}
          onClose={() => setCreating(false)}/>
      )}
      {confirmDelete && (
        <ConfirmModal
          title={`Supprimer la marque « ${confirmDelete.name} » ?`}
          message={
            confirmDelete.count > 0
              ? `${confirmDelete.count} produit${confirmDelete.count > 1 ? 's perdent' : ' perd'} sa marque (le champ marque est vidé, les produits restent). Cette action est irréversible.`
              : `Cette marque n'a aucun produit associé. Suppression du logo et de la description.`
          }
          danger
          onCancel={() => setConfirmDelete(null)}
          onConfirm={async () => {
            const b = confirmDelete;
            setConfirmDelete(null);
            await deleteBrand(b);
          }}
        />
      )}
    </div>
  );
};

const BrandMetaEditor = ({ brand, initial, onSave, onClose, onRename, existingNames = [] }) => {
  const isCreate = !brand;
  const [name, setName] = useAs(brand?.name || '');
  const [logo, setLogo] = useAs(initial.logo || '');
  const [description, setDescription] = useAs(initial.description || '');
  const [saving, setSaving] = useAs(false);
  const fileRef = useAr(null);

  // Resize an image to <= 256px on the longest side and re-encode as PNG —
  // keeps the brand_meta setting small even with high-res logos uploaded.
  const onPickFile = (file) => {
    if (!file) return;
    const reader = new FileReader();
    reader.onload = (e) => {
      const img = new Image();
      img.onload = () => {
        const max = 256;
        const scale = Math.min(1, max / Math.max(img.width, img.height));
        const w = Math.round(img.width * scale);
        const h = Math.round(img.height * scale);
        const canvas = document.createElement('canvas');
        canvas.width = w; canvas.height = h;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, w, h);
        setLogo(canvas.toDataURL('image/png'));
      };
      img.src = e.target.result;
    };
    reader.readAsDataURL(file);
  };

  const submit = async () => {
    const trimmed = name.trim();
    if (isCreate) {
      if (!trimmed) { pushToast('Le nom est requis', 'err'); return; }
    }
    setSaving(true);
    try {
      // Edit mode + name changed → rename cascade first, then save meta.
      if (!isCreate && onRename && trimmed && trimmed !== brand.name) {
        const conflict = existingNames.some(n =>
          n.toLowerCase() === trimmed.toLowerCase() && n.toLowerCase() !== brand.name.toLowerCase()
        );
        if (conflict) { pushToast('Une marque porte déjà ce nom', 'err'); return; }
        await onRename(brand.name, trimmed);
        await onSave({ logo: logo || null, description: description.trim() || null }, trimmed);
        return;
      }
      await onSave({ logo: logo || null, description: description.trim() || null }, trimmed);
    } finally { setSaving(false); }
  };

  return (
    <div className="ws-modal-back" onClick={onClose}>
      <div className="ws-modal" onClick={e => e.stopPropagation()} style={{ maxWidth: 520 }}>
        <h3 style={{ margin: 0 }}>{isCreate ? 'Nouvelle marque' : brand.name}</h3>
        <p className="ws-muted" style={{ fontSize: 12, marginTop: 4 }}>
          {isCreate
            ? 'Ajoutez une marque au catalogue. Les produits pourront être tagués avec ce nom.'
            : `${brand.count} produit${brand.count > 1 ? 's' : ''} référencé${brand.count > 1 ? 's' : ''} sous cette marque.`}
        </p>
        <div style={{ marginTop: 16 }}>
          <label style={{ fontSize: 12, color: 'var(--muted)' }}>Nom de la marque *</label>
          <input value={name} onChange={e => setName(e.target.value)}
            placeholder="Ex : Schneider, Legrand, Philips…"
            style={{ width: '100%', padding: 10, fontFamily: 'inherit', fontSize: 14, border: '1px solid var(--line)', background: 'var(--surface)', marginTop: 4 }}/>
          {!isCreate && name.trim() !== brand.name && name.trim() !== '' && (
            <p style={{ fontSize: 11, color: 'var(--accent)', marginTop: 4 }}>
              <Icon name="warn" size={11}/> {brand.count} produit{brand.count > 1 ? 's seront renommés' : ' sera renommé'} de « {brand.name} » à « {name.trim()} ».
            </p>
          )}
        </div>
        <div style={{ marginTop: 16 }}>
          <label style={{ fontSize: 12, color: 'var(--muted)' }}>Logo (PNG, JPG, SVG)</label>
          <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 6 }}>
            <div style={{ width: 80, height: 80, background: 'var(--surface)', border: '1px solid var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
              {logo
                ? <img src={logo} alt="" style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }}/>
                : <Icon name="tag" size={28} style={{ color: 'var(--muted)' }}/>}
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 6, flex: 1 }}>
              <input ref={fileRef} type="file" accept="image/png,image/jpeg,image/webp,image/svg+xml"
                style={{ display: 'none' }}
                onChange={(e) => onPickFile(e.target.files[0])}/>
              <button className="ws-btn ws-btn-ghost" onClick={() => fileRef.current?.click()}>
                <Icon name="pkg" size={12}/> Choisir un fichier
              </button>
              {logo && <button className="ws-btn ws-btn-ghost" onClick={() => setLogo('')}>Retirer</button>}
            </div>
          </div>
        </div>
        <div style={{ marginTop: 16 }}>
          <label style={{ fontSize: 12, color: 'var(--muted)' }}>Description</label>
          <textarea value={description} onChange={e => setDescription(e.target.value)}
            rows={4} placeholder="Ex : fabricant français de matériel électrique professionnel…"
            style={{ width: '100%', padding: 10, fontFamily: 'inherit', fontSize: 13, border: '1px solid var(--line)', background: 'var(--surface)', marginTop: 4 }}/>
        </div>
        <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 18 }}>
          <button className="ws-btn ws-btn-ghost" onClick={onClose}>Annuler</button>
          <button className="ws-btn ws-btn-primary" disabled={saving} onClick={submit}>
            {saving ? 'Enregistrement…' : 'Enregistrer'}
          </button>
        </div>
      </div>
    </div>
  );
};


// ==================================================================
//  Bulk product import (CSV / XLSX / PDF, no API, no recurring cost)
// ==================================================================
//  Pipeline: pick target category → upload file → auto-parse → editable
//  review table → bulk createProduct. PDF/XLSX libraries are pulled on
//  demand from a public CDN; no subscription, no API key.

const loadScript = (src) => new Promise((resolve, reject) => {
  if (document.querySelector(`script[data-import-src="${src}"]`)) return resolve();
  const s = document.createElement('script');
  s.src = src; s.async = true; s.dataset.importSrc = src;
  s.onload = resolve; s.onerror = () => reject(new Error('Échec du chargement : ' + src));
  document.head.appendChild(s);
});

const PDFJS_VER = '3.11.174';   // last UMD-friendly release
const loadPdfJs = async () => {
  if (window.pdfjsLib) return window.pdfjsLib;
  await loadScript(`https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PDFJS_VER}/pdf.min.js`);
  window.pdfjsLib.GlobalWorkerOptions.workerSrc =
    `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PDFJS_VER}/pdf.worker.min.js`;
  return window.pdfjsLib;
};
const loadXLSX = async () => {
  if (window.XLSX) return window.XLSX;
  await loadScript('https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js');
  return window.XLSX;
};

// ---------- CSV ----------
const parseCSV = (text) => {
  const rows = [];
  let cur = [], buf = '', q = false;
  for (let i = 0; i < text.length; i++) {
    const c = text[i];
    if (q) {
      if (c === '"' && text[i + 1] === '"') { buf += '"'; i++; }
      else if (c === '"') q = false;
      else buf += c;
    } else {
      if (c === '"') q = true;
      else if (c === ',' || c === ';' || c === '\t') { cur.push(buf); buf = ''; }
      else if (c === '\n' || c === '\r') {
        if (buf || cur.length) { cur.push(buf); rows.push(cur); cur = []; buf = ''; }
        if (c === '\r' && text[i + 1] === '\n') i++;
      } else buf += c;
    }
  }
  if (buf || cur.length) { cur.push(buf); rows.push(cur); }
  return rows.filter(r => r.some(v => v && v.trim()));
};

// Map CSV/XLSX rows to product objects using flexible header matching.
const HEADER_ALIASES = {
  nom:    ['nom', 'name', 'designation', 'désignation', 'product', 'produit', 'libellé', 'libelle'],
  ref:    ['ref', 'référence', 'reference', 'sku', 'code'],
  marque: ['marque', 'brand', 'fabricant', 'manufacturer'],
  stock:  ['stock', 'qty', 'quantity', 'quantité', 'qte'],
  prix:   ['prix', 'price', 'pu', 'tarif'],
  desc:   ['desc', 'description', 'descriptif', 'details', 'détails'],
  image_url: ['image', 'image_url', 'photo', 'url'],
};
const matchHeader = (h) => {
  const k = (h || '').toLowerCase().trim();
  for (const field in HEADER_ALIASES) {
    if (HEADER_ALIASES[field].some(a => k === a || k.includes(a))) return field;
  }
  return null;
};
const rowsToProducts = (rows) => {
  if (!rows.length) return [];
  const headers = rows[0].map(matchHeader);
  // If we couldn't identify any header, treat row 0 as data and use positional names.
  const usable = headers.some(Boolean);
  const dataRows = usable ? rows.slice(1) : rows;
  return dataRows.map(r => {
    const p = { nom: '', ref: '', marque: '', stock: 0, prix: 0, desc: '', image_url: '' };
    if (usable) {
      r.forEach((cell, i) => {
        const f = headers[i];
        if (!f) return;
        const v = (cell || '').toString().trim();
        if (f === 'stock' || f === 'prix') p[f] = Number(v.replace(',', '.')) || 0;
        else p[f] = v;
      });
    } else {
      // Best-effort positional: nom, ref, marque, stock
      [p.nom, p.ref, p.marque, p.stock] = [r[0] || '', r[1] || '', r[2] || '', Number(r[3]) || 0];
    }
    return p;
  }).filter(p => p.nom || p.ref);
};

const parseXLSX = async (file) => {
  const XLSX = await loadXLSX();
  const buf = await file.arrayBuffer();
  const wb = XLSX.read(buf, { type: 'array' });
  const sheet = wb.Sheets[wb.SheetNames[0]];
  const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '' });
  return rowsToProducts(rows.map(r => r.map(v => v == null ? '' : String(v))));
};

// ---------- PDF ----------
const parsePDF = async (file, onProgress) => {
  const lib = await loadPdfJs();
  const buf = await file.arrayBuffer();
  const pdf = await lib.getDocument({ data: buf }).promise;
  const pages = [];
  for (let i = 1; i <= pdf.numPages; i++) {
    onProgress && onProgress(i, pdf.numPages);
    const page = await pdf.getPage(i);
    const content = await page.getTextContent();
    const text = content.items.map(it => it.str).join(' ').replace(/\s+/g, ' ').trim();
    const images = await extractPdfPageImages(page, lib).catch(() => []);
    pages.push({ pageNum: i, text, images });
  }
  return pages;
};

// Extract embedded images from a PDF page (best effort across pdf.js versions).
const extractPdfPageImages = async (page, lib) => {
  const ops = await page.getOperatorList();
  const out = [];
  const wanted = new Set([lib.OPS.paintImageXObject, lib.OPS.paintInlineImageXObject].filter(x => x != null));
  for (let i = 0; i < ops.fnArray.length; i++) {
    if (!wanted.has(ops.fnArray[i])) continue;
    const arg = ops.argsArray[i][0];
    let img;
    try {
      img = await new Promise(res => {
        const v = page.objs.get(arg, x => res(x));
        if (v) res(v);
      });
    } catch (_) {}
    if (!img) continue;
    const w = img.width || img.bitmap?.width;
    const h = img.height || img.bitmap?.height;
    if (!w || !h || w < 50 || h < 50) continue;
    const canvas = document.createElement('canvas');
    canvas.width = w; canvas.height = h;
    const ctx = canvas.getContext('2d');
    try {
      if (img.bitmap)            ctx.drawImage(img.bitmap, 0, 0);
      else if (img.data && img.data.length) {
        const id = ctx.createImageData(w, h);
        // pdf.js gives RGB; ImageData wants RGBA — pad with 255 alpha.
        if (img.data.length === w * h * 3) {
          for (let p = 0, q = 0; q < img.data.length; p += 4, q += 3) {
            id.data[p]     = img.data[q];
            id.data[p + 1] = img.data[q + 1];
            id.data[p + 2] = img.data[q + 2];
            id.data[p + 3] = 255;
          }
        } else if (img.data.length === w * h * 4) {
          id.data.set(img.data);
        }
        ctx.putImageData(id, 0, 0);
      } else continue;
      out.push(canvas.toDataURL('image/jpeg', 0.85));
    } catch (_) {}
  }
  return out;
};

// Heuristic: split a stream of text into product blocks. Looks for reference
// patterns (alphanumeric codes like AO1501010, EKO-15W, REF.123) and uses
// each occurrence as a block boundary. Imperfect — admin reviews afterwards.
const REF_PATTERN = /\b([A-Z0-9]{2,}[-./]?[A-Z0-9]+(?:[-./][A-Z0-9]+)*)\b/g;
const splitTextIntoProducts = (text) => {
  const matches = [];
  let m;
  const re = new RegExp(REF_PATTERN.source, 'g');
  while ((m = re.exec(text)) !== null) {
    if (m[0].length >= 5 && /\d/.test(m[0])) matches.push({ ref: m[0], idx: m.index });
  }
  if (matches.length < 2) return [{ nom: text.slice(0, 200).trim(), ref: '', desc: text.trim() }];
  const products = [];
  for (let i = 0; i < matches.length; i++) {
    const start = matches[i].idx;
    const end = i + 1 < matches.length ? matches[i + 1].idx : text.length;
    const chunk = text.slice(start, end).trim();
    const ref = matches[i].ref;
    const after = chunk.slice(ref.length).trim();
    const firstSentence = after.split(/[.\n]/)[0].trim().slice(0, 120);
    products.push({
      nom: firstSentence || ref,
      ref,
      marque: '',
      stock: 0,
      prix: 0,
      desc: after,
      image_url: '',
    });
  }
  return products;
};

// ---------- Web scraper (free CORS proxies, no API key) ----------
// We try several public proxies in order. None require signup. If one is
// rate-limited or down, the next picks up. All free for life. Ordered by
// current reliability — the flaky ones (corsproxy.io / codetabs, which now
// hand back 403 / 522 on some hosts) sit at the back so we don't waste time
// on them before reaching one that works.
const WS_CORS_PROXIES = [
  (url) => 'https://api.allorigins.win/raw?url=' + encodeURIComponent(url),
  (url) => 'https://proxy.cors.sh/' + url,
  (url) => 'https://thingproxy.freeboard.io/fetch/' + url,
  (url) => 'https://api.codetabs.com/v1/proxy?quest=' + encodeURIComponent(url),
  (url) => 'https://corsproxy.io/?url=' + encodeURIComponent(url),
];
// A public proxy can hand back HTTP 200 whose *body* is actually its own
// error (Cloudflare "error code: 522", a 5xx HTML page, an empty string).
// If we returned that, the caller would parse garbage and give up instead of
// trying the next proxy — the exact failure we saw on Shopify's JSON APIs.
// So we sniff the body and reject known proxy-error shapes.
const looksLikeProxyError = (text) => {
  if (!text || text.length < 30) return true;
  const head = text.slice(0, 600).toLowerCase();
  return /error code:\s*\d/.test(head)
      || /\b(500 internal server error|502 bad gateway|503 service|504 gateway|522|cloudflare)\b/.test(head)
      || /just a moment|checking your browser|attention required/.test(head);
};

// Fetch `url` through the proxy chain. We RACE all proxies at once and take
// the first one that returns a valid body — rather than trying them serially,
// where a single dead proxy would burn its whole timeout before the next.
// This is the main speed lever: total latency ≈ the fastest working proxy
// (~2-5s) instead of the sum of every failing one (~2min worst case).
// `validate(text)` may reject a body (used by the JSON fetcher to skip proxies
// that mangle the response); a rejected body just loses the race.
const fetchViaProxy = (url, timeoutMs = 20000, validate = null) => {
  const errors = [];
  const attempts = WS_CORS_PROXIES.map((mk, i) => {
    const ctrl = new AbortController();
    const t = setTimeout(() => ctrl.abort('timeout'), timeoutMs);
    return fetch(mk(url), {
      signal: ctrl.signal,
      headers: { 'User-Agent': 'Mozilla/5.0 (compatible; WattSpotImporter/1.0)' },
    })
      .then(async (r) => {
        if (!r.ok) throw new Error('HTTP ' + r.status);
        const text = await r.text();
        if (looksLikeProxyError(text)) throw new Error('réponse proxy invalide');
        if (validate && validate(text) === false) throw new Error('contenu inattendu');
        return text;
      })
      .catch((e) => {
        errors.push(`proxy ${i + 1}: ${e.name === 'AbortError' ? 'timeout' : (e.message || e)}`);
        throw e;                                   // stays a rejection so Promise.any skips it
      })
      .finally(() => clearTimeout(t));
  });
  // Promise.any → first fulfilled wins; rejects only if ALL fail.
  return Promise.any(attempts).catch(() => {
    throw new Error('Tous les proxies ont échoué (' + errors.join(' · ') + ')');
  });
};

// Extract a JSON value from a proxy body: parse directly, else salvage the
// first {...}/[...] span (some proxies prepend/append junk).
const parseJsonLoose = (text) => {
  try { return JSON.parse(text); } catch (_) {}
  const start = text.search(/[[{]/);
  if (start > 0) { try { return JSON.parse(text.slice(start)); } catch (_) {} }
  return undefined;
};

// Fetch + JSON.parse via the proxy chain. Crucially, we validate parseability
// per-proxy: if allorigins returns an error page, we retry cors.sh for the
// same URL rather than aborting the whole Shopify import.
const fetchJsonViaProxy = async (url, timeoutMs = 20000) => {
  const text = await fetchViaProxy(url, timeoutMs, (t) => parseJsonLoose(t) !== undefined);
  const val = parseJsonLoose(text);
  if (val === undefined) throw new Error('réponse JSON invalide');
  return val;
};

// Parse a product page's HTML into { nom, ref, marque, prix, desc,
// image_url, images[] }. Layered strategy — each field tries:
//   1. JSON-LD Product schema     (cleanest, on modern e-commerce)
//   2. itemprop / dedicated DOM nodes
//   3. Text patterns (Référence: …, prices, etc.)
//   4. OpenGraph / meta fallback
const extractProductFromHtml = (html, sourceUrl) => {
  const doc = new DOMParser().parseFromString(html, 'text/html');
  const out = { nom: '', ref: '', marque: '', stock: 0, prix: 0, desc: '', image_url: '', images: [] };

  const og   = (prop) => doc.querySelector(`meta[property="${prop}"]`)?.getAttribute('content') || '';
  const meta = (name) => doc.querySelector(`meta[name="${name}"]`)?.getAttribute('content') || '';

  // Pick the best resolution from an <img>.
  const bestSrc = (img) => {
    if (!img) return '';
    const srcset = img.getAttribute('srcset') || img.getAttribute('data-srcset');
    if (srcset) {
      const variants = srcset.split(',').map(v => {
        const m = v.trim().match(/^(\S+)(?:\s+(\d+)w)?/);
        return m ? { url: m[1], w: parseInt(m[2]) || 0 } : null;
      }).filter(Boolean);
      if (variants.length) {
        variants.sort((a, b) => b.w - a.w);
        if (variants[0].url) return variants[0].url;
      }
    }
    return img.getAttribute('data-zoom-image')
        || img.getAttribute('data-large_image')
        || img.getAttribute('data-large-image')
        || img.getAttribute('data-full')
        || img.getAttribute('data-original')
        || img.getAttribute('data-lazy-src')
        || img.getAttribute('data-src')
        || img.getAttribute('src')
        || '';
  };

  const collectLdImages = (val) => {
    if (!val) return [];
    if (typeof val === 'string') return [val];
    if (Array.isArray(val)) return val.flatMap(collectLdImages);
    if (val.url) return [val.url];
    if (val.contentUrl) return [val.contentUrl];
    return [];
  };

  // ---- 1) JSON-LD Product schema ----
  const lds = [...doc.querySelectorAll('script[type="application/ld+json"]')];
  for (const node of lds) {
    let data; try { data = JSON.parse(node.textContent); } catch (_) { continue; }
    const queue = Array.isArray(data) ? [...data] : [data];
    while (queue.length) {
      const x = queue.shift();
      if (!x || typeof x !== 'object') continue;
      if (x['@graph']) queue.push(...x['@graph']);
      const t = (x['@type'] || '').toString().toLowerCase();
      if (t.includes('product')) {
        if (!out.nom    && x.name)        out.nom    = String(x.name).trim();
        if (!out.desc   && x.description) out.desc   = String(x.description).trim();
        if (!out.ref    && (x.sku || x.mpn || x.gtin)) out.ref = String(x.sku || x.mpn || x.gtin).trim();
        if (!out.marque) out.marque = (typeof x.brand === 'string' ? x.brand : x.brand?.name || '').toString().trim();
        const ldImgs = collectLdImages(x.image);
        if (ldImgs.length) out.images.push(...ldImgs);
        const offer = Array.isArray(x.offers) ? x.offers[0] : x.offers;
        if (offer?.price && !out.prix) out.prix = Number(offer.price) || 0;
      }
    }
  }

  // ---- 2) NAME — pick the longest of h1 / og:title / <title> ----
  const cleanText = (s) => (s || '').replace(/\s+/g, ' ').trim();
  const titleClean = cleanText(doc.title || '').replace(/\s*[-|·–—]\s*[^-|·–—]+$/, '');
  const h1Texts = [...doc.querySelectorAll('h1.product-title, h1.product_title, h1[itemprop="name"], h1.entry-title, h1')].map(h => cleanText(h.textContent));
  const candidates = [...h1Texts, cleanText(og('og:title')), titleClean]
    .filter(Boolean)
    .filter(s => s.length < 250);
  candidates.sort((a, b) => b.length - a.length);
  if (candidates[0]) {
    if (!out.nom || candidates[0].length > out.nom.length) out.nom = candidates[0];
  }
  out.nom = (out.nom || '').slice(0, 250);

  // ---- 3) REFERENCE / SKU ----
  if (!out.ref) {
    const skuEl = doc.querySelector(
      '[itemprop="sku"], [itemprop="mpn"], .sku, .product-sku, .product_meta .sku, ' +
      '.reference-value, [class*="reference"]:not(input):not(textarea)'
    );
    if (skuEl) {
      const t = cleanText(skuEl.textContent).replace(/^[\s\w]*?[:\s]+/, '').trim();
      if (t && t.length < 80 && /[A-Z0-9]/i.test(t)) out.ref = t;
    }
  }
  if (!out.ref) {
    const body = (doc.body?.textContent || '').slice(0, 8000);
    const patterns = [
      /\bR[ée]f[ée]rence\s*:?\s*([\w][\w\-\.\/]{2,40})/i,
      /\bR[ée]f\.?\s+([\w][\w\-\.\/]{2,40})/i,
      /\bSKU\s*:?\s*([\w][\w\-\.\/]{2,40})/i,
      /\bModel\s*:?\s*([\w][\w\-\.\/]{2,40})/i,
      /\bArticle\s*N°?\s*:?\s*([\w][\w\-\.\/]{2,40})/i,
      /\bCode\s*:?\s*([\w][\w\-\.\/]{2,40})/i,
    ];
    for (const re of patterns) { const m = body.match(re); if (m) { out.ref = m[1].trim(); break; } }
  }
  if (!out.ref) {
    const blob = (out.nom + ' ' + (doc.body?.textContent || '').slice(0, 2000));
    const m = blob.match(/\b([A-Z][A-Z0-9]{2,}(?:[-_./][A-Z0-9]+)+)\b/);
    if (m) out.ref = m[1];
  }

  // ---- 4) MARQUE ----
  if (!out.marque) {
    const brandEl = doc.querySelector(
      '[itemprop="brand"], .product-brand, .product_brand, .brand-name, ' +
      '[class*="product-brand"], [class*="ProductBrand"]'
    );
    if (brandEl) {
      const t = cleanText(brandEl.textContent);
      if (t && t.length < 60) out.marque = t;
    }
  }
  if (!out.marque) {
    const brandLink = doc.querySelector('a[href*="/marque/"], a[href*="/marques/"], a[href*="/brand/"], a[href*="/brands/"]');
    if (brandLink) {
      const t = cleanText(brandLink.textContent);
      if (t && t.length < 60) out.marque = t;
    }
  }
  if (!out.marque) {
    // Image with a brand-like alt next to (or inside) the product summary
    const summary = doc.querySelector('.product-summary, .summary, .product-info, [class*="product-summary"]') || doc.body;
    const brandImg = [...summary.querySelectorAll('img[alt]')].find(img => {
      const alt = (img.getAttribute('alt') || '').trim();
      const src = (img.getAttribute('src') || '').toLowerCase();
      return alt && alt.length > 1 && alt.length < 40
        && !/logo|menu|search|cart|panier/i.test(alt)
        && (/marque|brand|logo/i.test(src) || /brand|marque/i.test(img.className || ''));
    });
    if (brandImg) out.marque = brandImg.getAttribute('alt').trim();
  }
  if (!out.marque) out.marque = og('product:brand') || og('og:brand') || '';

  // ---- 5) PRICE ----
  if (!out.prix) {
    const priceEl = doc.querySelector(
      '[itemprop="price"], .price-current, .current-price, ' +
      '.price ins .amount, .price > .amount, ' +
      '[class*="price-amount"], [class*="ProductPrice"]'
    );
    if (priceEl) {
      const raw = priceEl.getAttribute('content') || priceEl.textContent || '';
      const m = raw.replace(/\s/g, '').match(/(\d+([.,]\d+)?)/);
      if (m) out.prix = parseFloat(m[1].replace(',', '.')) || 0;
    }
  }

  // ---- 6) DESCRIPTION — collect every plausible candidate, keep the longest ----
  // We split into "specific" and "broad". Broad selectors match large layout
  // containers (entry-content, article…) that may swallow the whole page;
  // we only fall back to those if specifics yield <60 chars.
  const SPECIFIC_DESC = [
    '[itemprop="description"]',
    '.woocommerce-product-details__short-description',
    '.woocommerce-Tabs-panel--description',
    '.woocommerce-Tabs-panel',
    '#tab-description', '#tab-additional_information', '#tab-specifications',
    '[id^="tab-"][role="tabpanel"]', '[role="tabpanel"][id*="description" i]',
    '.product-description', '.product__description', '.product-detail-description',
    '#description', '#product-description', '.description',
    '.product-short-description', '.short-description',
    '.product-tabs__content', '.product-detail__content',
    '.tab-content > .active', '.tab-pane.active',
    '.elementor-tab-content', '.elementor-tabs-content',
    '.product-info__description',
  ];
  const BROAD_DESC = [
    '[role="tabpanel"]',
    '.tab-content', '.tab-pane',
    '.product-details', '.product-info', '.product-summary',
    '.entry-content',
  ];
  const candidate = (sels) => {
    let best = '';
    for (const sel of sels) {
      doc.querySelectorAll(sel).forEach(el => {
        if (el === doc.body) return;
        const text = cleanText(el.textContent);
        if (text.length > best.length && text.length < 6000) best = text;
      });
    }
    return best;
  };
  let bestDesc = candidate(SPECIFIC_DESC);
  if (bestDesc.length < 60) {
    const broad = candidate(BROAD_DESC);
    if (broad.length > bestDesc.length) bestDesc = broad;
  }
  if (bestDesc.length > out.desc.length) out.desc = bestDesc;
  if (!out.desc) out.desc = og('og:description') || meta('description') || '';

  // ---- 7) IMAGES — gather from gallery containers, then dedup ----
  const seen = new Set(out.images.map(u => (u || '').replace(/[?#].*$/, '')));
  const pushImg = (url) => {
    if (!url) return;
    if (/^data:image\/svg/i.test(url)) return;
    if (/^data:/.test(url)) return;
    if (/icon|sprite|placeholder|favicon|loading|spinner|avatar|emoji|paypal|visa|mastercard|whatsapp/i.test(url)) return;
    if (/\/logo[\.\-_]/i.test(url) && !/product|article/i.test(url)) return;
    let abs = url;
    try { abs = new URL(url, sourceUrl).toString(); } catch (_) {}
    const key = abs.replace(/[?#].*$/, '');
    if (seen.has(key)) return;
    seen.add(key);
    out.images.push(abs);
  };

  const GALLERY_SELECTORS = [
    '.product-gallery', '.product-images', '.product__media', '.product__gallery',
    '.gallery', '.product-photos',
    '[class*="gallery"]', '[class*="product-image"]', '[class*="product-media"]',
    '[data-gallery]', '.fotorama', '.swiper', '.slick', '.glide',
    '.product-thumbs', '.thumbnails', '.thumbs',
    '.woocommerce-product-gallery', '.woocommerce-product-gallery__wrapper',
    'figure.product-image',
  ].join(', ');
  const scopes = [...doc.querySelectorAll(GALLERY_SELECTORS)];
  scopes.forEach(scope => {
    [...scope.querySelectorAll('img')].forEach(img => pushImg(bestSrc(img)));
    // Anchors that point straight to a hi-res JPG/PNG (lightbox links)
    [...scope.querySelectorAll('a[href]')].forEach(a => {
      const href = a.getAttribute('href');
      if (href && /\.(jpe?g|png|webp|gif)(\?|$)/i.test(href)) pushImg(href);
    });
  });
  // Fallback: scan main / body if we still have <2 images.
  if (out.images.length < 2) {
    const main = doc.querySelector('main, [role="main"], #main, .main') || doc.body;
    [...main.querySelectorAll('img')].forEach(img => pushImg(bestSrc(img)));
  }
  pushImg(og('og:image'));

  out.images = out.images
    .map(u => { try { return new URL(u, sourceUrl).toString(); } catch (_) { return u; } })
    .slice(0, 12);
  out.image_url = out.images[0] || '';

  if (out.desc && out.desc.length > 4000) out.desc = out.desc.slice(0, 4000) + '…';
  return out;
};

// ---------- Shopify fast-path ----------
// A large share of stores run Shopify, which exposes clean JSON with no HTML
// parsing needed: /products.json (catalog), /search/suggest.json (search),
// /products/{handle}.js (single product). When we detect it we pull straight
// from those endpoints — far more reliable than scraping the rendered page,
// which is often JS-hydrated and yields no links to the DOM heuristics.
const htmlToText = (html) => {
  if (!html) return '';
  try {
    const doc = new DOMParser().parseFromString(html, 'text/html');
    return (doc.body?.textContent || '').replace(/\s+/g, ' ').trim();
  } catch (_) {
    return String(html).replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
  }
};

// Map one Shopify product object (from products.json / .js / suggest.json)
// into our { nom, ref, marque, prix, desc, image_url, images } shape.
const shopifyProductToItem = (p, origin) => {
  const variant = (p.variants && p.variants[0]) || {};
  const rawPrice = variant.price ?? p.price ?? 0;
  // products.json gives price as a decimal string ("199.000"); the .js and
  // suggest.json endpoints give price in cents (integer). Normalise both.
  let prix = 0;
  if (typeof rawPrice === 'string') prix = parseFloat(rawPrice) || 0;
  else if (typeof rawPrice === 'number') prix = rawPrice > 1000 ? rawPrice / 100 : rawPrice;
  const imgs = (p.images || [])
    .map(im => (typeof im === 'string' ? im : (im.src || im)))
    .filter(Boolean)
    .map(u => (u.startsWith('//') ? 'https:' + u : u));
  if (!imgs.length && p.featured_image) {
    const f = typeof p.featured_image === 'string' ? p.featured_image : p.featured_image.src;
    if (f) imgs.push(f.startsWith('//') ? 'https:' + f : f);
  }
  let desc = htmlToText(p.body_html || p.description || '');
  if (desc.length > 4000) desc = desc.slice(0, 4000) + '…';
  const item = {
    nom: (p.title || '').trim().slice(0, 250),
    ref: (variant.sku || p.sku || '').toString().trim(),
    marque: (p.vendor || '').toString().trim(),
    stock: variant.available === false ? 0 : (variant.inventory_quantity ?? 0),
    prix,
    desc,
    image_url: imgs[0] || '',
    images: imgs.slice(0, 12),
  };
  if (p.handle && origin) item._sourceUrl = origin.replace(/\/$/, '') + '/products/' + p.handle;
  return item;
};

// Given a Shopify store URL, discover which products to import.
//  - /search?...&q=…      → use the store's search suggest API for that query
//  - /collections/{slug}  → page through that collection's products.json
//  - anything else         → page through the whole catalogue's products.json
// Returns [] when the site isn't Shopify so the caller can fall back to HTML.
const scrapeShopify = async (pageUrl, log) => {
  let u; try { u = new URL(pageUrl); } catch (_) { return null; }
  const origin = u.origin;

  // Cheap capability probe: does /products.json exist and look like Shopify?
  let isShopify = false;
  try {
    const probe = await fetchJsonViaProxy(origin + '/products.json?limit=1', 20000);
    isShopify = !!(probe && Array.isArray(probe.products));
  } catch (_) { return null; }
  if (!isShopify) return null;

  log?.('🛍  Site Shopify détecté — utilisation de l\'API JSON native');

  // Page through a products.json endpoint (whole store or one collection).
  // Returns the raw Shopify product objects (not yet mapped to our shape).
  // Fetch a products.json feed. Pages are requested in parallel BATCHES (we
  // can't know the last page up front, so we grab `BATCH` pages at once, then
  // stop as soon as a batch comes back short — i.e. we've hit the end). This
  // cuts a multi-page catalogue from N sequential round-trips to ~N/BATCH.
  const fetchCatalog = async (base, maxPages = 20) => {
    const prods = [];
    const BATCH = 4;
    for (let start = 1; start <= maxPages; start += BATCH) {
      const pageNums = [];
      for (let p = start; p < start + BATCH && p <= maxPages; p++) pageNums.push(p);
      const results = await Promise.all(pageNums.map(p =>
        fetchJsonViaProxy(`${base}?limit=250&page=${p}`)
          .then(d => d?.products || [])
          .catch(() => null)                         // null = this page failed
      ));
      let hitEnd = false;
      for (const batch of results) {
        if (batch === null) { hitEnd = true; continue; }  // treat failure as end-of-feed
        prods.push(...batch);
        if (batch.length < 250) hitEnd = true;            // short page = last page
      }
      log?.(`📦 ${prods.length} produit${prods.length > 1 ? 's' : ''} chargé${prods.length > 1 ? 's' : ''}…`);
      if (hitEnd) break;
    }
    return prods;
  };

  // Score a Shopify product against free-text search terms (title, tags, sku,
  // vendor, type). Used as a local fallback when suggest.json is unavailable.
  const matchesQuery = (p, terms) => {
    const hay = [
      p.title, p.product_type, p.vendor,
      Array.isArray(p.tags) ? p.tags.join(' ') : p.tags,
      (p.variants || []).map(v => v.sku).join(' '),
    ].filter(Boolean).join(' ').toLowerCase();
    return terms.every(t => hay.includes(t));
  };

  // --- Search results ---
  const q = u.searchParams.get('q');
  if (/\/search\b/.test(u.pathname) && q) {
    // 1) Try Shopify's native search API (best relevance ranking).
    const api = origin + '/search/suggest.json?q=' + encodeURIComponent(q) +
      '&resources%5Btype%5D=product&resources%5Blimit%5D=10' +
      '&resources%5Boptions%5D%5Bunavailable_products%5D=last';
    let hits = [];
    try {
      const data = await fetchJsonViaProxy(api);
      hits = data?.resources?.results?.products || [];
    } catch (e) {
      log?.('⚠︎ API de recherche indisponible — filtrage du catalogue…');
    }

    // 2) Fallback: pull the whole catalogue and filter it locally. The search
    //    API rate-limits/500s through public proxies far more than the plain
    //    catalogue does, so this keeps search working when suggest.json is down.
    if (!hits.length) {
      const terms = q.toLowerCase().split(/\s+/).filter(Boolean);
      // Cap the scan: paging the whole store through a public proxy is slow
      // (~20s per 250 products). 6 pages / 1500 products is a sane ceiling for
      // a search fallback; the collection & catalogue modes below are uncapped.
      const fullProducts = await fetchCatalog(origin + '/products.json', 6);
      const matched = fullProducts.filter(p => matchesQuery(p, terms));
      log?.(`🔎 ${matched.length} produit${matched.length > 1 ? 's' : ''} correspondant à « ${q} » (sur ${fullProducts.length})`);
      // These already carry body_html/sku/images — map directly, no refetch.
      return matched.map(p => shopifyProductToItem(p, origin));
    }

    log?.(`🔎 ${hits.length} résultat${hits.length > 1 ? 's' : ''} de recherche pour « ${q} »`);
    // suggest.json records are lightweight (no body_html/sku). Enrich each from
    // its full .js payload — all at once (Promise.all) instead of one-by-one,
    // so 10 products cost ~1 round-trip rather than 10. Fall back to the light
    // record if a given .js call fails.
    const items = await Promise.all(
      hits.filter(h => h.handle).map(async (hit) => {
        try {
          const full = await fetchJsonViaProxy(origin + '/products/' + hit.handle + '.js');
          log?.('✓ ' + (full.title || hit.title || hit.handle).slice(0, 60));
          return shopifyProductToItem(full, origin);
        } catch (_) {
          log?.('✓ ' + (hit.title || hit.handle).slice(0, 60) + ' (aperçu)');
          return shopifyProductToItem(hit, origin);
        }
      })
    );
    return items;
  }

  // --- Collection or whole catalogue ---
  const collMatch = u.pathname.match(/\/collections\/([^/]+)/);
  const base = collMatch ? `${origin}/collections/${collMatch[1]}/products.json` : `${origin}/products.json`;
  const prods = await fetchCatalog(base);
  return prods.map(p => shopifyProductToItem(p, origin));
};

// Find product links on a category/listing page.
// Strategy: filter out nav / header / footer / sidebar / cookie banners,
// then look for anchors that *look like product cards* — i.e. they contain
// both an <img> and a <h1..h6>, or a "Voir le produit"-style CTA.
// Falls back to common e-commerce selectors only if nothing matched.
const findProductLinks = (html, baseUrl, customSelector) => {
  const doc = new DOMParser().parseFromString(html, 'text/html');
  const baseHost = (() => { try { return new URL(baseUrl).hostname; } catch (_) { return ''; } })();
  const baseObj = new URL(baseUrl);
  const seen = new Set();

  // Anchors inside these containers are virtually never products.
  const SKIP_SELECTOR = [
    'header', 'nav', 'footer', 'aside',
    '[role="navigation"]', '[role="banner"]', '[role="contentinfo"]', '[role="menubar"]',
    '[class*="menu"]', '[class*="navigation"]', '[class*="navbar"]', '[class*="sidebar"]',
    '[class*="cookie"]', '[class*="breadcrumb"]', '[class*="footer"]', '[class*="header"]',
  ].join(', ');

  const acceptUrl = (href) => {
    if (!href) return null;
    const h = href.trim();
    if (h.startsWith('#') || h.startsWith('mailto:') || h.startsWith('tel:') || h.startsWith('javascript:')) return null;
    try {
      const u = new URL(h, baseUrl);
      if (u.hostname !== baseHost) return null;
      if (u.pathname === baseObj.pathname) return null;
      if (/\/(category|categorie|categories|tag|tags|search|cart|panier|checkout|compte|account|login|register|sign|page|sitemap|wp-content|wp-json|wp-admin|feed|contact|about|a-propos)\b/i.test(u.pathname)) return null;
      // Strip query/hash so duplicates collapse
      u.search = ''; u.hash = '';
      return u.toString();
    } catch (_) { return null; }
  };

  const tryAdd = (a) => {
    if (a.closest(SKIP_SELECTOR)) return;
    const url = acceptUrl(a.getAttribute('href'));
    if (url) seen.add(url);
  };

  if (customSelector?.trim()) {
    doc.querySelectorAll(customSelector.trim()).forEach(tryAdd);
    return [...seen].slice(0, 200);
  }

  // A "title-like" node — many themes put the product name in a <span>/<div>
  // with a *title / *name class rather than in a real heading, so requiring a
  // strict <h1..h6> (as we used to) missed whole storefronts, Shopify in
  // particular. Accept either.
  const hasTitle = (a) => !!a.querySelector(
    'h1, h2, h3, h4, h5, h6, ' +
    '[class*="title" i], [class*="name" i], [class*="product-card__" i], [itemprop="name"]'
  );

  // Pass 1 — card-like anchors: contain an <img> and a title-ish node.
  doc.querySelectorAll('a[href]').forEach(a => {
    if (a.querySelector('img') && hasTitle(a)) tryAdd(a);
  });

  // Pass 2 — explicit product-URL patterns. This is the single most reliable
  // signal across platforms (Shopify /products/, Woo /product/, French shops
  // /produit/, Magento/Presta /item/ & /p/, plus generic "-p-"/".html" leaves).
  // We run it unconditionally and merge, because well-built stores expose these
  // links even when their card markup doesn't match the heuristics above.
  doc.querySelectorAll(
    'a[href*="/products/"], a[href*="/product/"], a[href*="/produit/"], ' +
    'a[href*="/produits/"], a[href*="/item/"], a[href*="/p/"], ' +
    'ul.products li.product a, a.woocommerce-LoopProduct-link, ' +
    '[class*="product" i] a[href], [class*="card" i] a[href], article a[href]'
  ).forEach(a => {
    const href = a.getAttribute('href') || '';
    // For the broad container selectors, keep only anchors that actually point
    // at something product-shaped (avoids "add to cart" / quick-view buttons).
    if (/\/(products?|produits?|item|p)\//i.test(href) || /-p-\d|\.html?($|\?)/i.test(href)
        || a.querySelector('img')) tryAdd(a);
  });

  // Pass 3 — "Voir le produit" / "See product" CTA links (last resort).
  if (seen.size < 3) {
    const CTA = /^(voir le produit|voir plus|découvrir|en savoir plus|consulter|see (more|product|details)|view (more|product|details)|read more)$/i;
    doc.querySelectorAll('a[href]').forEach(a => {
      const txt = (a.textContent || '').replace(/\s+/g, ' ').trim();
      if (txt && CTA.test(txt)) tryAdd(a);
    });
  }

  return [...seen].slice(0, 200);
};

const AdminProductImport = ({ cats, onBack }) => {
  const leaves = flattenLeaves(cats);
  const [step, setStep] = useAs(1);                 // 1=cat, 2=file, 3=review
  const [targetCat, setTargetCat] = useAs('');
  const [busy, setBusy] = useAs(false);
  const [progress, setProgress] = useAs('');
  const [items, setItems] = useAs([]);
  const [imageBank, setImageBank] = useAs([]);     // { dataUrl, page }
  const [pasted, setPasted] = useAs('');
  const [scrapeUrl, setScrapeUrl] = useAs('');
  const [scrapeSelector, setScrapeSelector] = useAs('');
  const [scrapeLog, setScrapeLog] = useAs([]);
  const scrapeStopRef = useAr(false);
  const fileRef = useAr(null);

  const targetLabel = leaves.find(l => l.id === targetCat)?.label || '';

  const handleFile = async (file) => {
    setBusy(true); setProgress('Lecture du fichier…'); setItems([]); setImageBank([]);
    try {
      const name = file.name.toLowerCase();
      if (name.endsWith('.csv') || name.endsWith('.txt')) {
        const text = await file.text();
        setItems(rowsToProducts(parseCSV(text)));
      } else if (name.endsWith('.xlsx') || name.endsWith('.xls')) {
        setProgress('Analyse Excel…');
        setItems(await parseXLSX(file));
      } else if (name.endsWith('.pdf')) {
        const pages = await parsePDF(file, (i, t) => setProgress(`Page ${i}/${t}…`));
        const allText = pages.map(p => p.text).join(' ');
        const products = splitTextIntoProducts(allText);
        const bank = [];
        pages.forEach(pg => pg.images.forEach(dataUrl => bank.push({ dataUrl, page: pg.pageNum })));
        setImageBank(bank);
        setItems(products);
      } else {
        pushToast('Format non reconnu — utilisez CSV, XLSX ou PDF', 'err');
        return;
      }
      setStep(3);
    } catch (e) {
      pushToast('Erreur de lecture : ' + (e.message || e), 'err');
    } finally {
      setBusy(false); setProgress('');
    }
  };

  const parsePastedText = () => {
    if (!pasted.trim()) return;
    setBusy(true);
    try {
      const looksCSV = pasted.includes(',') || pasted.includes(';') || pasted.includes('\t');
      const products = looksCSV ? rowsToProducts(parseCSV(pasted)) : splitTextIntoProducts(pasted);
      setItems(products);
      setStep(3);
    } finally { setBusy(false); }
  };

  // Scrape a category / listing URL: discover product links, fetch each one,
  // pull whatever structured data is available. Bounded concurrency to be a
  // good citizen with the proxy.
  const runScrape = async () => {
    if (!scrapeUrl.trim()) return;
    setBusy(true); setScrapeLog([]); scrapeStopRef.current = false; setItems([]); setImageBank([]);
    const log = (msg) => setScrapeLog(l => [...l, msg]);
    let stopped = false;
    try {
      // Fast-path: Shopify stores serve clean JSON — no HTML heuristics needed.
      // Returns null when the site isn't Shopify (or its API is blocked), and
      // we fall through to the generic HTML scraper below.
      setProgress('Analyse du site…');
      const shopify = await scrapeShopify(scrapeUrl, log).catch(() => null);
      if (shopify && shopify.length) {
        setItems(shopify);
        setStep(3);
        pushToast(`${shopify.length} produit${shopify.length > 1 ? 's' : ''} extrait${shopify.length > 1 ? 's' : ''}`);
        return;
      }

      log(`📥 Téléchargement de la page de listing…`);
      setProgress('Listing…');
      const listHtml = await fetchViaProxy(scrapeUrl);
      const links = findProductLinks(listHtml, scrapeUrl, scrapeSelector);
      log(`🔗 ${links.length} lien${links.length > 1 ? 's' : ''} produit détecté${links.length > 1 ? 's' : ''}`);
      if (!links.length) { pushToast('Aucun lien produit détecté — vérifiez l\'URL ou le sélecteur', 'err'); return; }

      const out = [];
      let done = 0;
      const CONCURRENCY = 8;                 // proxies are raced now, so we can push more in flight
      const queue = [...links];
      const workers = Array.from({ length: CONCURRENCY }, async () => {
        while (queue.length) {
          if (scrapeStopRef.current || stopped) { stopped = true; return; }
          const url = queue.shift();
          try {
            const html = await fetchViaProxy(url);
            const product = extractProductFromHtml(html, url);
            product._sourceUrl = url;
            if (product.nom || product.ref) out.push(product);
            done++;
            setProgress(`${done}/${links.length} pages…`);
            log(`✓ ${product.nom?.slice(0, 60) || url}`);
          } catch (e) {
            done++;
            log(`✗ ${url.split('/').slice(-2).join('/')}: ${e.message}`);
          }
        }
      });
      await Promise.all(workers);
      if (stopped) log(`⏸ Arrêté à ${done}/${links.length}`);
      setItems(out);
      setStep(3);
      pushToast(`${out.length} produit${out.length > 1 ? 's' : ''} extrait${out.length > 1 ? 's' : ''}`);
    } catch (e) {
      pushToast('Erreur scraping : ' + (e.message || e), 'err');
      log(`❌ ${e.message || e}`);
    } finally {
      setBusy(false); setProgress('');
    }
  };

  const updateRow = (i, patch) => setItems(items.map((it, j) => j === i ? { ...it, ...patch } : it));
  const removeRow = (i) => setItems(items.filter((_, j) => j !== i));
  const addBlank = () => setItems([...items, { nom: '', ref: '', marque: '', stock: 0, prix: 0, desc: '', image_url: '' }]);

  const importAll = async () => {
    if (!targetCat) { pushToast('Choisissez la catégorie', 'err'); return; }
    const valid = items.filter(it => it.nom && it.nom.trim());
    if (!valid.length) { pushToast('Aucun produit valide (le nom est requis)', 'err'); return; }
    setBusy(true);

    setProgress('Préparation de la catégorie…');
    await ensureCategoryPath(targetCat, cats);

    // Verify the category row now exists before inserting products — otherwise
    // every insert fails identically on products_category_id_fkey. If it's
    // still missing, stop with a message the admin can act on.
    try {
      const dbCats = await window.WS_DB.listCategories();
      if (Array.isArray(dbCats) && dbCats.length && !dbCats.some(c => c.id === targetCat)) {
        setBusy(false); setProgress('');
        pushToast(`Catégorie « ${targetLabel || targetCat} » introuvable en base — impossible de créer la catégorie. Vérifiez la connexion Supabase.`, 'err');
        return;
      }
    } catch (_) { /* if the check itself fails, fall through and let inserts report */ }

    let ok = 0; const errors = [];
    for (let i = 0; i < valid.length; i++) {
      setProgress(`Import ${i + 1}/${valid.length}…`);
      try {
        const desc = valid[i].desc || '';
        const parsed = (desc && window.parseDescription)
          ? window.parseDescription(desc, valid[i].nom)
          : null;
        await window.WS_DB.createProduct({
          cat: targetCat,
          nom: valid[i].nom,
          ref: valid[i].ref || '',
          marque: valid[i].marque || '',
          stock: Number(valid[i].stock) || 0,
          stockStatus: (Number(valid[i].stock) || 0) > 0 ? 'disponible' : 'rupture',
          prix: Number(valid[i].prix) || 0,
          desc: desc,
          description_parsed: parsed,
          image_url: valid[i].image_url || '',
          images: Array.isArray(valid[i].images) ? valid[i].images : [],
          active: true,
          web_visible: true,
          mobile_visible: true,
        });
        ok++;
      } catch (e) {
        errors.push({ row: i + 1, msg: e.message || String(e) });
      }
    }
    setBusy(false); setProgress('');

    if (errors.length) {
      console.error('[import] failures:', errors);
      pushToast(`${ok} importés · ${errors.length} échecs · ${errors[0].msg}`, 'err');
      // Stay on the review page so the admin can inspect / retry.
      return;
    }
    pushToast(`${ok} produit${ok > 1 ? 's' : ''} importé${ok > 1 ? 's' : ''}`);
    onBack(true);
  };

  return (
    <div>
      <div className="adm-page-head">
        <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
          <button className="ws-btn ws-btn-ghost" onClick={() => onBack(false)}><Icon name="chevL" size={14}/> Retour</button>
          <div>
            <h1 style={{ margin: 0 }}>Importer en masse</h1>
            <p className="ws-muted" style={{ margin: 0, fontSize: 12 }}>
              CSV, Excel (.xlsx) ou PDF · 100 % local, aucune API
            </p>
          </div>
        </div>
        {targetLabel && step >= 2 && (
          <span className="ws-chip ws-chip-default" title="Catégorie cible"><Icon name="tag" size={12}/> {targetLabel}</span>
        )}
      </div>

      {/* Step 1: target category */}
      {step === 1 && (
        <div className="adm-card" style={{ padding: 24 }}>
          <h3 style={{ marginTop: 0 }}>1. Catégorie cible</h3>
          <p className="ws-muted" style={{ fontSize: 13 }}>Tous les produits importés seront classés dans cette catégorie. Vous pourrez en modifier individuellement plus tard.</p>
          <div style={{ maxWidth: 520, margin: '12px 0' }}>
            <LeafCategoryPicker cats={cats} value={targetCat} onChange={setTargetCat} placeholder="— Sélectionner —"/>
          </div>
          <div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
            <button className="ws-btn ws-btn-primary" disabled={!targetCat} onClick={() => setStep(2)}>Continuer <Icon name="chevR" size={12}/></button>
          </div>
        </div>
      )}

      {/* Step 2: file or paste */}
      {step === 2 && (
        <div className="adm-card" style={{ padding: 24 }}>
          <h3 style={{ marginTop: 0 }}>2. Fichier source</h3>
          <p className="ws-muted" style={{ fontSize: 13 }}>
            <strong>CSV / Excel</strong> : colonnes recommandées <code>nom, ref, marque, stock, prix, description, image_url</code>.
            <br/><strong>PDF</strong> : le script extrait le texte et les images, puis vous corrigez. Idéal pour les listes de prix structurées ; les catalogues complexes nécessitent plus de retouches.
          </p>

          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginTop: 16 }}>
            {/* File drop */}
            <div
              onClick={() => fileRef.current?.click()}
              onDrop={(e) => { e.preventDefault(); const f = e.dataTransfer.files[0]; if (f) handleFile(f); }}
              onDragOver={(e) => e.preventDefault()}
              style={{
                border: '2px dashed var(--line)', padding: 32, textAlign: 'center',
                cursor: 'pointer', background: 'var(--surface)',
              }}>
              <Icon name="pkg" size={32} style={{ color: 'var(--accent)' }}/>
              <div style={{ marginTop: 10, fontWeight: 600 }}>Glissez un fichier ou cliquez</div>
              <div className="ws-muted" style={{ fontSize: 12, marginTop: 4 }}>.csv · .xlsx · .pdf</div>
              <input ref={fileRef} type="file" accept=".csv,.xlsx,.xls,.pdf,.txt"
                style={{ display: 'none' }}
                onChange={(e) => e.target.files[0] && handleFile(e.target.files[0])}/>
            </div>
            {/* Paste */}
            <div>
              <label style={{ fontSize: 12, color: 'var(--muted)' }}>…ou collez un tableau / CSV</label>
              <textarea value={pasted} onChange={(e) => setPasted(e.target.value)}
                placeholder={'nom,ref,marque,stock,description\nSpot LED 15W,EKO-15W,Ekoled,100,Spot encastré...'}
                style={{ width: '100%', minHeight: 200, padding: 10, fontFamily: 'monospace', fontSize: 12, border: '1px solid var(--line)', background: 'var(--surface)', marginTop: 4 }}/>
              <button className="ws-btn ws-btn-ghost" disabled={!pasted.trim() || busy} onClick={parsePastedText} style={{ marginTop: 8 }}>
                Analyser le texte
              </button>
            </div>
          </div>

          <div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
            <button className="ws-btn ws-btn-ghost" onClick={() => setStep(1)}><Icon name="chevL" size={12}/> Retour</button>
          </div>

          {/* ---- Scraper option ---- */}
          <div style={{ marginTop: 24, paddingTop: 24, borderTop: '1px solid var(--line)' }}>
            <h3 style={{ margin: '0 0 6px' }}>…ou scraper un site web</h3>
            <p className="ws-muted" style={{ fontSize: 13, marginTop: 0 }}>
              Donnez l'URL d'une page <strong>liste de produits</strong>, d'une <strong>collection</strong>, d'une <strong>catégorie</strong> ou même d'une page de <strong>recherche</strong>. Les boutiques Shopify sont lues via leur API JSON native (données propres, très fiable) ; les autres sites (WooCommerce, PrestaShop…) via JSON-LD / OpenGraph / heuristiques DOM. Aucune clé d'API — passe par un proxy CORS public gratuit.
            </p>

            <div style={{ display: 'grid', gridTemplateColumns: '1fr 280px auto', gap: 8, alignItems: 'flex-end', marginTop: 12 }}>
              <div>
                <label style={{ fontSize: 12, color: 'var(--muted)' }}>URL de la page liste *</label>
                <input value={scrapeUrl} onChange={e => setScrapeUrl(e.target.value)}
                  placeholder="https://example.com/categorie/spots/"
                  style={{ width: '100%', padding: '8px 10px', border: '1px solid var(--line)', background: 'var(--surface)', marginTop: 4 }}/>
              </div>
              <div>
                <label style={{ fontSize: 12, color: 'var(--muted)' }}>Sélecteur CSS (optionnel)</label>
                <input value={scrapeSelector} onChange={e => setScrapeSelector(e.target.value)}
                  placeholder="ex: .product-card a"
                  style={{ width: '100%', padding: '8px 10px', border: '1px solid var(--line)', background: 'var(--surface)', marginTop: 4 }}/>
              </div>
              <div>
                {busy
                  ? <button className="ws-btn ws-btn-ghost" onClick={() => { scrapeStopRef.current = true; }}>Arrêter</button>
                  : <button className="ws-btn ws-btn-primary" disabled={!scrapeUrl.trim()} onClick={runScrape}>
                      <Icon name="search" size={12}/> Scraper
                    </button>}
              </div>
            </div>

            {scrapeLog.length > 0 && (
              <pre style={{
                marginTop: 12, padding: 10, background: 'var(--surface)', border: '1px solid var(--line)',
                fontSize: 11, fontFamily: 'monospace', maxHeight: 200, overflowY: 'auto',
                whiteSpace: 'pre-wrap', wordBreak: 'break-all',
              }}>{scrapeLog.join('\n')}</pre>
            )}
          </div>

          {busy && <div style={{ marginTop: 12 }}>{progress || 'Traitement…'}</div>}
        </div>
      )}

      {/* Step 3: review + save */}
      {step === 3 && (
        <>
          <div className="adm-card" style={{ padding: 18, marginBottom: 12 }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
              <div>
                <strong>{items.length}</strong> produit{items.length > 1 ? 's' : ''} extrait{items.length > 1 ? 's' : ''}
                {imageBank.length > 0 && <> · <strong>{imageBank.length}</strong> image{imageBank.length > 1 ? 's' : ''} disponibles ↓</>}
              </div>
              <div style={{ display: 'flex', gap: 8 }}>
                <button className="ws-btn ws-btn-ghost" onClick={() => setStep(2)}>Recharger un autre fichier</button>
                <button className="ws-btn ws-btn-ghost" onClick={addBlank}><Icon name="plus" size={12}/> Ajouter une ligne</button>
                <button className="ws-btn ws-btn-primary" disabled={busy} onClick={importAll}>
                  {busy ? (progress || 'Import…') : `Importer ${items.length} produit${items.length > 1 ? 's' : ''}`}
                </button>
              </div>
            </div>
          </div>

          <div className="adm-card" style={{ padding: 0, overflow: 'auto' }}>
            <table className="ws-table adm-table">
              <thead>
                <tr>
                  <th style={{ width: 70 }}>Image</th>
                  <th>Nom *</th>
                  <th style={{ width: 130 }}>Réf</th>
                  <th style={{ width: 130 }}>Marque</th>
                  <th style={{ width: 80 }}>Stock</th>
                  <th style={{ width: 90 }}>Prix</th>
                  <th>Description</th>
                  <th style={{ width: 40 }}></th>
                </tr>
              </thead>
              <tbody>
                {items.map((it, i) => (
                  <tr key={i}>
                    <td>
                      {it.image_url
                        ? <div style={{ position: 'relative', width: 50, height: 50 }}>
                            <img src={it.image_url} alt="" style={{ width: 50, height: 50, objectFit: 'cover', cursor: 'pointer' }}
                              onClick={() => updateRow(i, { image_url: '', images: [] })} title="Cliquez pour retirer"/>
                            {it.images?.length > 1 && (
                              <span style={{ position: 'absolute', top: 0, right: 0, background: 'var(--accent)', color: '#fff', fontSize: 10, padding: '1px 4px', fontWeight: 700 }}>
                                +{it.images.length - 1}
                              </span>
                            )}
                          </div>
                        : <div style={{ width: 50, height: 50, background: 'var(--surface)', border: '1px dashed var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                            <span style={{ fontSize: 10, color: 'var(--muted)' }}>—</span>
                          </div>}
                    </td>
                    <td><input value={it.nom} onChange={e => updateRow(i, { nom: e.target.value })} style={importInput}/></td>
                    <td><input value={it.ref} onChange={e => updateRow(i, { ref: e.target.value })} style={importInput}/></td>
                    <td><input value={it.marque} onChange={e => updateRow(i, { marque: e.target.value })} style={importInput}/></td>
                    <td><input type="number" value={it.stock} onChange={e => updateRow(i, { stock: e.target.value })} style={importInput}/></td>
                    <td><input type="number" step="0.001" value={it.prix} onChange={e => updateRow(i, { prix: e.target.value })} style={importInput}/></td>
                    <td><input value={it.desc} onChange={e => updateRow(i, { desc: e.target.value })} style={importInput}/></td>
                    <td><button className="adm-row-btn" onClick={() => removeRow(i)} title="Supprimer"><Icon name="trash" size={12}/></button></td>
                  </tr>
                ))}
                {items.length === 0 && (
                  <tr><td colSpan="8" style={{ padding: 30, textAlign: 'center' }} className="ws-muted">Aucun produit extrait</td></tr>
                )}
              </tbody>
            </table>
          </div>

          {imageBank.length > 0 && (
            <div className="adm-card" style={{ padding: 18, marginTop: 12 }}>
              <h3 style={{ marginTop: 0 }}>Banque d'images extraites du PDF</h3>
              <p className="ws-muted" style={{ fontSize: 12 }}>
                Cliquez une image puis cliquez la cellule "Image" d'un produit pour l'assigner.
              </p>
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(80px, 1fr))', gap: 8, maxHeight: 280, overflowY: 'auto' }}>
                {imageBank.map((img, idx) => (
                  <ImageBankItem key={idx} img={img} onPick={(dataUrl) => {
                    // Find first row without image and assign
                    const target = items.findIndex(it => !it.image_url);
                    if (target >= 0) updateRow(target, { image_url: dataUrl });
                    else pushToast('Toutes les lignes ont déjà une image — retirez-en une d\'abord');
                  }}/>
                ))}
              </div>
            </div>
          )}
        </>
      )}
    </div>
  );
};

const importInput = {
  width: '100%', padding: '6px 8px', border: '1px solid var(--line)',
  background: 'var(--surface)', color: 'var(--text)', fontSize: 12.5,
};

const ImageBankItem = ({ img, onPick }) => (
  <div onClick={() => onPick(img.dataUrl)}
    style={{ cursor: 'pointer', border: '1px solid var(--line)', overflow: 'hidden', position: 'relative' }}
    title={`Page ${img.page} — cliquer pour assigner`}>
    <img src={img.dataUrl} alt="" style={{ width: '100%', height: 80, objectFit: 'cover', display: 'block' }}/>
    <span style={{ position: 'absolute', bottom: 2, right: 2, background: 'rgba(0,0,0,0.6)', color: '#fff', padding: '1px 5px', fontSize: 10 }}>p.{img.page}</span>
  </div>
);

Object.assign(window, {
  AdminShell, AdminDashboard, AdminOrders, AdminStock,
  AdminProducts, AdminCats, AdminBrands, AdminCustomers, AdminReports,
  ToastHost, pushToast: (...a) => pushToast(...a),
});
