// AXLU — screens 2: Syncs list, sync detail, catalog list (with gallery + Shopify col + CSV import)

// Local Stat helper (small KPI tile) so we don't depend on KPI prop shape.
function Stat({ label, value, tone = "neutral" }) {
  const toneCls = tone === "success" ? "text-emerald-600"
                : tone === "danger"  ? "text-red-600"
                : tone === "info"    ? "accent-fg"
                : tone === "warning" ? "text-amber-600"
                : "text-app";
  return (
    <div className="surface border rounded-md p-3" style={{ borderColor: "var(--border)" }}>
      <div className="text-[11px] uppercase tracking-wider text-subtle font-medium">{label}</div>
      <div className={`text-[20px] font-semibold mono tabular-nums mt-1 ${toneCls}`}>{value}</div>
    </div>
  );
}

// ─── Syncs list (global) ───────────────────────────────────────────────
function SyncsList({ navigate }) {
  const v = useAxluStore();
  const { SYNCS, SUPPLIERS, SYNC_STATUSES } = window.AxluData;
  const [supplierF, setSupplierF] = useState("all");
  const [statusF, setStatusF] = useState("all");
  const [search, setSearch] = useState("");
  const [startOpen, setStartOpen] = useState(false);
  const [startAll, setStartAll] = useState(false);
  const [launching, setLaunching] = useState(false);
  const [startSupplier, setStartSupplier] = useState(SUPPLIERS[0]?.id || "");
  const [startType, setStartType] = useState(() => {
    // feeds[0] is a feed OBJECT ({type,url,...}) — take its `type` string only.
    const f0 = SUPPLIERS[0] && SUPPLIERS[0].feeds && SUPPLIERS[0].feeds[0];
    return (f0 && f0.type) || (typeof f0 === 'string' ? f0 : '') || "products";
  });
  const toast = useToast();

  const filtered = useMemo(() => SYNCS.filter((s) =>
    (supplierF === "all" || s.supplierId === supplierF) &&
    (statusF === "all" || s.status === statusF) &&
    (!search || s.id.toLowerCase().includes(search.toLowerCase()))
  ), [v, SYNCS, supplierF, statusF, search]);

  const fmtDuration = (sec) => sec == null ? "—" : sec < 60 ? `${sec}s` : `${Math.floor(sec/60)}m ${sec%60}s`;

  // Launch the REAL feed import (fetch + parse + apply), exactly like
  // Fournisseurs → « Lancer toutes les synchros ». startAll = every feed of
  // the supplier; otherwise just the one picked. (No more simulated sync.)
  const launchSync = async () => {
    if (launching || !startSupplier) return;
    const supplier = SUPPLIERS.find((s) => s.id === startSupplier);
    if (!supplier) { toast("Fournisseur introuvable", { tone: "danger" }); return; }
    if (typeof runSupplierFeedsInOrder !== "function") { toast("Import indisponible (relancez l'app)", { tone: "danger" }); return; }
    setStartOpen(false);
    setLaunching(true);
    const runOpts = startAll ? { toast } : { toast, onlyTypes: [startType] };
    toast(startAll ? "Synchronisation de tous les flux lancée…" : `Synchronisation « ${flowLabel(startType)} » lancée…`, { tone: "info" });
    let keepLaunching = false;
    try {
      const r = await runSupplierFeedsInOrder(supplier, runOpts);
      if (r && r.jobStarted) { keepLaunching = true; setTimeout(() => setLaunching(false), 125000); } // import en arrière-plan : on garde l'indicateur "en cours"
      else if (!r || r.total === 0) toast("Aucun flux prêt pour ce fournisseur (vérifiez les URL dans Fournisseurs → Flux)", { tone: "warning" });
      else toast(`Synchronisation terminée — ${r.ran || 0}/${r.total} flux` + (r.failed ? ` · ${r.failed} en échec` : ""), { tone: r.failed ? "warning" : "success" });
    } catch (e) {
      toast("Échec : " + (e && e.message ? e.message : String(e)), { tone: "danger" });
    } finally {
      if (!keepLaunching) setLaunching(false);
    }
  };

  const startSupplierObj = SUPPLIERS.find((s) => s.id === startSupplier);
  const supplierFeeds = startSupplierObj ? window.AxluStore.normalizeFeeds(startSupplierObj) : [];
  const feedOpts = supplierFeeds.length > 0
    ? supplierFeeds.map((f) => ({ value: f.type, label: flowLabel(f.type) }))
    : [{ value: "products", label: flowLabel("products") }];
  useEffect(() => {
    if (startSupplierObj) {
      const types = supplierFeeds.map((f) => f.type);
      if (types.length > 0 && !types.includes(startType)) {
        setStartType(types[0]);
      }
    }
  }, [startSupplier]);

  return (
    <div>
      <PageHeader
        breadcrumb={[{ label: "Synchronisations" }]}
        title="Synchronisations"
        subtitle={`${SYNCS.length} synchros sur les 30 derniers jours`}
        actions={
          <Button variant="primary" leftIcon={launching ? "sync" : "play"} disabled={launching}
                  onClick={() => { setStartAll(false); setStartOpen(true); }}>
            {launching ? "Synchronisation…" : "Lancer une synchro"}
          </Button>
        }
      />

      <Modal open={startOpen} onClose={() => setStartOpen(false)} title="Lancer une synchronisation"
             footer={<>
               <Button variant="ghost" onClick={() => setStartOpen(false)}>Annuler</Button>
               <Button variant="primary" leftIcon="play" onClick={launchSync}
                       disabled={!startSupplier || (!startAll && !startType)}>
                 {startAll ? "Lancer toutes les synchros" : "Lancer"}
               </Button>
             </>}>
        <div className="space-y-3">
          <Field label="1 · Fournisseur">
            <Select value={startSupplier} onChange={setStartSupplier}
                    options={SUPPLIERS.map((s) => ({ value: s.id, label: s.name }))}/>
          </Field>
          <label className="flex items-center gap-2 cursor-pointer select-none">
            <Checkbox checked={startAll} onChange={() => setStartAll((a) => !a)}/>
            <span className="text-[13px]">
              Lancer <strong>toutes</strong> les synchros de ce fournisseur
              {supplierFeeds.length > 0 ? ` (${supplierFeeds.length})` : ""}
            </span>
          </label>
          {!startAll && (
            <Field label="2 · Synchronisation à lancer">
              <Select value={startType} onChange={setStartType} options={feedOpts}/>
            </Field>
          )}
        </div>
      </Modal>

      <div className="p-6">
        <div className="flex items-center gap-2 mb-4 flex-wrap">
          <Input leftIcon="search" placeholder="ID de synchro…" value={search} onChange={(e) => setSearch(e.target.value)} className="w-[260px]"/>
          <Select value={supplierF} onChange={setSupplierF} options={[
            { value: "all", label: "Tous fournisseurs" },
            ...SUPPLIERS.map((s) => ({ value: s.id, label: s.name })),
          ]}/>
          <Select value={statusF} onChange={setStatusF} options={[
            { value: "all", label: "Tous statuts" },
            ...Object.entries(SYNC_STATUSES).map(([k, def]) => ({ value: k, label: def.label })),
          ]}/>
          <div className="flex-1"/>
          <span className="text-[12px] text-muted">{filtered.length} sur {SYNCS.length}</span>
        </div>

        <Card padding="none">
          <table className="axlu-table">
            <thead>
              <tr>
                <th>ID</th><th>Fournisseur</th><th>Type</th><th>Démarré</th><th>Durée</th>
                <th className="text-right">Lus</th>
                <th className="text-right">Créés</th>
                <th className="text-right">M-à-j</th>
                <th className="text-right">Erreurs</th>
                <th>Statut</th>
                <th></th>
              </tr>
            </thead>
            <tbody>
              {filtered.map((s) => {
                const supplier = SUPPLIERS.find((x) => x.id === s.supplierId);
                const flowEmoji = window.AxluData.FLOW_EMOJI[s.type] || "📥";
                return (
                  <tr key={s.id} className="hover-row cursor-pointer" onClick={() => navigate(`#/syncs/${s.id}`)}>
                    <td className="mono text-[12px]">{s.id}</td>
                    <td>
                      <span className="inline-flex items-center gap-1.5">
                        <span aria-hidden>{supplier?.emoji}</span>
                        <span>{supplier?.name || "—"}</span>
                      </span>
                    </td>
                    <td><Badge tone="muted"><span className="mr-1" aria-hidden>{flowEmoji}</span>{s.type}</Badge></td>
                    <td className="mono text-[12px] text-muted">{fmt.date(s.startedAt)}</td>
                    <td className="mono text-[12px] text-muted">{fmtDuration(s.duration)}</td>
                    <td className="text-right mono tabular-nums text-muted">{fmt.num(s.rowsRead)}</td>
                    <td className="text-right mono tabular-nums">{fmt.num(s.rowsCreated)}</td>
                    <td className="text-right mono tabular-nums">{fmt.num(s.rowsUpdated)}</td>
                    <td className="text-right mono tabular-nums">
                      {s.errors > 0 ? <span className="text-red-600">{fmt.num(s.errors)}</span> : <span className="text-subtle">0</span>}
                    </td>
                    <td><StatusBadge status={s.status} map={SYNC_STATUSES}/></td>
                    <td><IconButton icon="chevronRight" size="sm"/></td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </Card>
      </div>
    </div>
  );
}

// ─── Sync detail ───────────────────────────────────────────────────────
function SyncDetail({ syncId, navigate }) {
  const v = useAxluStore();
  const { SYNCS, SUPPLIERS, SYNC_STATUSES, SYNC_ERRORS } = window.AxluData;
  const sync = SYNCS.find((s) => s.id === syncId) || SYNCS[2];
  const supplier = SUPPLIERS.find((s) => s.id === sync.supplierId);
  const fmtDuration = (sec) => sec == null ? "en cours…" : sec < 60 ? `${sec}s` : `${Math.floor(sec/60)}m ${sec%60}s`;
  const toast = useToast();
  const [errOpen, setErrOpen] = useState(null);

  const downloadLogs = () => {
    const start = new Date(sync.startedAt).getTime();
    const lines = [];
    lines.push(`[${new Date(start).toISOString()}] [INFO] sync ${sync.id} started — supplier=${supplier?.name || sync.supplierId} type=${sync.type}`);
    const total = Math.max(30, Math.min(50, Math.floor((sync.rowsRead || 100) / 20) + 30));
    for (let i = 1; i < total - 2; i++) {
      const ts = new Date(start + i * 1000).toISOString();
      const isErr = sync.errors > 0 && i % Math.max(5, Math.floor(total / Math.max(1, sync.errors))) === 0;
      if (isErr) {
        lines.push(`[${ts}] [ERROR] row ${i * 20} — invalid value for field price`);
      } else if (i % 4 === 0) {
        lines.push(`[${ts}] [DEBUG] processed ${i * 20} rows…`);
      } else {
        lines.push(`[${ts}] [INFO] batch ${i} ok`);
      }
    }
    const endTs = new Date(start + (sync.duration || 60) * 1000).toISOString();
    lines.push(`[${endTs}] [INFO] read=${sync.rowsRead} created=${sync.rowsCreated} updated=${sync.rowsUpdated} errors=${sync.errors}`);
    lines.push(`[${endTs}] [INFO] sync ${sync.id} finished status=${sync.status}`);
    window.AxluStore.download(lines.join("\n"), `${sync.id}.log`, "text/plain");
  };

  const [retrying, setRetrying] = useState(false);
  const retrySync = async () => {
    if (retrying) return;
    if (!supplier) { toast("Fournisseur introuvable", { tone: "danger" }); return; }
    if (typeof runSupplierFeedsInOrder !== "function") { toast("Import indisponible (relancez l'app)", { tone: "danger" }); return; }
    setRetrying(true);
    toast(`Relance de « ${flowLabel(sync.type)} »…`, { tone: "info" });
    try {
      const r = await runSupplierFeedsInOrder(supplier, { toast, onlyTypes: [sync.type] });
      toast(`Relance terminée — ${r.ran || 0}/${r.total || 0} flux` + (r.failed ? ` · ${r.failed} en échec` : ""), { tone: r.failed ? "warning" : "success" });
    } catch (e) {
      toast("Échec : " + (e && e.message ? e.message : String(e)), { tone: "danger" });
    } finally { setRetrying(false); }
  };

  const cancelSync = () => {
    window.AxluStore.dispatch("sync.cancel", { id: sync.id });
    toast("Synchronisation annulée", { tone: "warning" });
  };

  const details = [
    ["Fournisseur",   supplier?.name || "—"],
    ["Type de flux",  sync.type],
    ["Démarré",       fmt.date(sync.startedAt)],
    ["Durée",         fmtDuration(sync.duration)],
    ["ID synchro",    <span className="mono text-[11px]">{sync.id}</span>],
  ];
  if (sync.errorMsg) details.push(["Erreur fatale", <span className="text-red-600">{sync.errorMsg}</span>]);

  return (
    <div>
      <PageHeader
        breadcrumb={[
          { label: "Synchros", onClick: () => navigate("#/syncs") },
          { label: sync.id },
        ]}
        title={
          <span className="flex items-center gap-3">
            <span className="mono">{sync.id}</span>
            <StatusBadge status={sync.status} map={SYNC_STATUSES}/>
          </span>
        }
        subtitle={`${supplier?.name || "—"} · ${sync.type} · démarrée ${fmt.date(sync.startedAt)} · durée ${fmtDuration(sync.duration)}`}
        actions={
          <>
            <Button variant="secondary" leftIcon="download" onClick={downloadLogs}>Télécharger les logs</Button>
            {sync.status === "failed" && <Button variant="primary" leftIcon="refresh" onClick={retrySync} disabled={retrying}>{retrying ? "Relance…" : "Relancer"}</Button>}
            {sync.status === "running" && <Button variant="danger" leftIcon="cross" onClick={cancelSync}>Annuler</Button>}
          </>
        }
      />

      <div className="p-6 grid grid-cols-12 gap-6">
        <div className="col-span-8 space-y-4">
          <div className="grid grid-cols-4 gap-3">
            <Stat label="Lignes lues"    value={fmt.num(sync.rowsRead)}/>
            <Stat label="Créés"          value={fmt.num(sync.rowsCreated)} tone="success"/>
            <Stat label="Mis à jour"     value={fmt.num(sync.rowsUpdated)} tone="info"/>
            <Stat label="Erreurs"        value={fmt.num(sync.errors)} tone={sync.errors > 0 ? "danger" : "neutral"}/>
          </div>

          {sync.errors > 0 && (
            <Card title="Erreurs détectées" padding="none"
                  action={<Badge tone="danger">{sync.errors}</Badge>}>
              <table className="axlu-table">
                <thead>
                  <tr><th>Ligne</th><th>SKU</th><th>Champ</th><th>Message</th><th></th></tr>
                </thead>
                <tbody>
                  {SYNC_ERRORS.map((e, i) => (
                    <tr key={i} className="hover-row">
                      <td className="mono text-[12px] text-muted">L{e.line}</td>
                      <td className="mono text-[12px]">{e.sku}</td>
                      <td><Badge tone="muted">{e.field}</Badge></td>
                      <td className="text-[12.5px] text-red-600">{e.message}</td>
                      <td className="text-right">
                        <Button size="sm" variant="ghost" onClick={() => setErrOpen(e)}>Voir</Button>
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </Card>
          )}
        </div>

        <div className="col-span-4 space-y-4">
          <Card title="Détails" padding="md">
            <div className="space-y-2 text-[12.5px]">
              {details.map(([k, v], i) => (
                <div key={i} className="flex items-start justify-between gap-3">
                  <span className="text-subtle">{k}</span>
                  <span className="text-app text-right">{v}</span>
                </div>
              ))}
            </div>
          </Card>

          <Card title="Bilan" padding="md">
            {sync.message && (
              <div className="mb-3 text-[12px] rounded-md px-3 py-2 sunken text-app">{sync.message}</div>
            )}
            <div className="space-y-2 text-[12.5px]">
              <div className="flex items-center justify-between"><span className="text-muted">Lignes lues</span><span className="mono tabular-nums">{fmt.num(sync.rowsRead)}</span></div>
              <div className="flex items-center justify-between"><span className="text-muted">Produits créés</span><span className="mono tabular-nums">{fmt.num(sync.rowsCreated)}</span></div>
              <div className="flex items-center justify-between"><span className="text-muted">Produits mis à jour</span><span className="mono tabular-nums">{fmt.num(sync.rowsUpdated)}</span></div>
              {sync.missing > 0 && (
                <div className="flex items-center justify-between"><span className="text-muted">Sans correspondance (SKU absent)</span><span className="mono tabular-nums text-amber-600">{fmt.num(sync.missing)}</span></div>
              )}
              {sync.flagged > 0 && (
                <div className="flex items-center justify-between"><span className="text-muted">Re-marqués « à vérifier »</span><span className="mono tabular-nums text-amber-600">{fmt.num(sync.flagged)}</span></div>
              )}
              <div className="flex items-center justify-between"><span className="text-muted">Erreurs</span><span className={`mono tabular-nums ${sync.errors > 0 ? "text-red-600" : ""}`}>{fmt.num(sync.errors)}</span></div>
            </div>
            {sync.missing > 0 && (
              <div className="mt-3 text-[11px] text-subtle leading-relaxed">
                « Sans correspondance » = lignes du flux dont le SKU n'existe dans aucun produit du catalogue (produit pas encore importé, ou code différent). Ce n'est pas une erreur bloquante.
              </div>
            )}
          </Card>
        </div>
      </div>

      <Modal open={!!errOpen} onClose={() => setErrOpen(null)} title="Détail de l'erreur"
             footer={<Button variant="primary" onClick={() => setErrOpen(null)}>Fermer</Button>}>
        {errOpen && (
          <pre className="text-[11.5px] mono p-3 sunken rounded-md overflow-auto" style={{ maxHeight: 360 }}>
{JSON.stringify(errOpen, null, 2)}
          </pre>
        )}
      </Modal>
    </div>
  );
}

// ─── Catalog list ──────────────────────────────────────────────────────
const SHOPIFY_PUB_DEF = {
  offline: { label: "Hors-ligne", tone: "muted",   dot: false },
  draft:   { label: "Brouillon",  tone: "warning", dot: true },
  active:  { label: "Actif",      tone: "success", dot: true },
  error:   { label: "Erreur",     tone: "danger",  dot: true },
};

function CatalogList({ navigate, initialStatus, initialMarginMax, initialLastImport }) {
  const v = useAxluStore();
  const { PRODUCTS, PRODUCT_STATUSES, CATEGORIES_AXLU, SUPPLIERS } = window.AxluData;

  const [view, setView] = useState("table"); // table | gallery | kanban
  const [search, setSearch] = useState("");
  const [statusFilter, setStatusFilter] = useState(initialStatus || "all");
  const [categoryFilter, setCategoryFilter] = useState("all");
  const [subCategoryFilter, setSubCategoryFilter] = useState("all");
  const [supplierFilter, setSupplierFilter] = useState("all");
  const [shopifyFilter, setShopifyFilter] = useState("all");
  const [hasImageFilter, setHasImageFilter] = useState("all");
  const [stockFilter, setStockFilter] = useState("all"); // all | in | out (rupture)
  const [priceMin, setPriceMin] = useState("");
  const [priceMax, setPriceMax] = useState("");
  const [marginMin, setMarginMin] = useState("");
  const [marginMax, setMarginMax] = useState(initialMarginMax || "");
  const [lastImportFilter, setLastImportFilter] = useState(initialLastImport === "1");
  const [selected, setSelected] = useState(new Set());
  const [bulkOpen, setBulkOpen] = useState(null);
  const [csvOpen, setCsvOpen] = useState(false);
  const [confirmDeleteRow, setConfirmDeleteRow] = useState(null);
  const [page, setPage] = useState(1);
  const PAGE_SIZE = 50;
  const toast = useToast();

  // Présence collaborative sur la page Catalogue → bloque les actions de masse si un AUTRE est là.
  React.useEffect(() => {
    if (!window.AxluStore.acquireLock) return;
    window.AxluStore.acquireLock('page:catalog');
    return () => window.AxluStore.releaseLock('page:catalog');
  }, []);
  const catalogBlockedBy = () => {
    const others = window.AxluStore.othersOnPage ? window.AxluStore.othersOnPage('catalog') : [];
    return (others && others.length) ? others[0] : null;
  };
  const canDelete = window.AxluStore.can("delete");

  useEffect(() => { if (initialStatus) setStatusFilter(initialStatus); }, [initialStatus]);
  useEffect(() => { if (initialMarginMax) setMarginMax(initialMarginMax); }, [initialMarginMax]);
  useEffect(() => { setLastImportFilter(initialLastImport === "1"); }, [initialLastImport]);

  // Category index — resolve a product's leaf category to its parent group.
  const catById = useMemo(() => {
    const m = new Map();
    CATEGORIES_AXLU.forEach((c) => m.set(c.id, c));
    return m;
  }, [v, CATEGORIES_AXLU]);
  // SKUs of products added by the last import — used to filter the catalog
  // when the user clicks « Voir » on the dashboard "X new products" alert.
  const addedSkusSet = useMemo(() => {
    const li = window.AxluData.LAST_IMPORT;
    return li && Array.isArray(li.addedSkus) ? new Set(li.addedSkus) : new Set();
  }, [v]);
  // A product matches when one of its categories is the chosen sub-category,
  // or — when no sub-category is chosen — belongs to the chosen group.
  const productInCategory = (p) => {
    if (categoryFilter === "all" && subCategoryFilter === "all") return true;
    const ids = (Array.isArray(p.categoryIds) && p.categoryIds.length)
      ? p.categoryIds : (p.categoryId ? [p.categoryId] : []);
    if (subCategoryFilter !== "all") return ids.includes(subCategoryFilter);
    return ids.some((cid) => {
      const c = catById.get(cid);
      return c && (c.id === categoryFilter || c.parent === categoryFilter);
    });
  };

  const filtered = useMemo(() => PRODUCTS.filter((p) =>
    (!lastImportFilter || addedSkusSet.has(p.sku)) &&
    (statusFilter === "all" || p.status === statusFilter) &&
    productInCategory(p) &&
    (supplierFilter === "all" || p.supplier === supplierFilter) &&
    (shopifyFilter === "all" || p.shopifyPub === shopifyFilter) &&
    (hasImageFilter === "all" || (hasImageFilter === "yes" ? p.hasImage : !p.hasImage)) &&
    // Stock: "out" = vraie rupture (0 et pas "disponible"); "in" = stock réel ou disponible.
    (stockFilter === "all"
      || (stockFilter === "out"
            ? (!p.stockUncounted && (p.stock || 0) === 0)
            : (p.stockUncounted || (p.stock || 0) > 0))) &&
    (!search || p.title.toLowerCase().includes(search.toLowerCase()) || p.sku.toLowerCase().includes(search.toLowerCase())) &&
    (!priceMin || p.sellPrice >= parseFloat(priceMin)) &&
    (!priceMax || p.sellPrice <= parseFloat(priceMax)) &&
    (!marginMin || p.marginPct >= parseFloat(marginMin)) &&
    (!marginMax || (p.marginPct != null && p.marginPct <= parseFloat(marginMax)))
  ), [v, PRODUCTS, statusFilter, categoryFilter, subCategoryFilter, catById, supplierFilter, shopifyFilter, hasImageFilter, stockFilter, search, priceMin, priceMax, marginMin, marginMax, lastImportFilter, addedSkusSet]);

  const { sorted, sort, toggle } = useSorted(filtered, "title");

  const lastPage = Math.max(1, Math.ceil(sorted.length / PAGE_SIZE));
  useEffect(() => { if (page > lastPage) setPage(1); }, [lastPage, page]);
  const paginated = useMemo(() => sorted.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE), [sorted, page]);

  const toggleSelect = (id) => {
    const next = new Set(selected);
    next.has(id) ? next.delete(id) : next.add(id);
    setSelected(next);
  };
  const toggleAll = () => {
    if (selected.size === paginated.length) setSelected(new Set());
    else setSelected(new Set(paginated.map((p) => p.id)));
  };

  const applyBulk = (action, payload) => {
    const ids = [...selected];
    let res;
    switch (action) {
      case "approve":   window.AxluStore.dispatch("product.bulkSetStatus", { ids, status: "approved" }); break;
      case "review":    window.AxluStore.dispatch("product.bulkSetStatus", { ids, status: "to_review" }); break;
      case "block":     window.AxluStore.dispatch("product.bulkSetStatus", { ids, status: "blocked" }); break;
      case "archive":   window.AxluStore.dispatch("product.bulkSetStatus", { ids, status: "archived" }); break;
      case "publish":   res = window.AxluStore.dispatch("product.bulkPublish",   { ids, mode: "publish" }); break;
      case "republish": res = window.AxluStore.dispatch("product.bulkPublish",   { ids, mode: "republish" }); break;
      case "activate":   window.AxluStore.dispatch("product.bulkPublish",   { ids, mode: "activate" }); break;
      case "deactivate": window.AxluStore.dispatch("product.bulkPublish",   { ids, mode: "deactivate" }); break;
      case "unpublish": window.AxluStore.dispatch("product.bulkPublish",   { ids, mode: "unpublish" }); break;
      case "category":  window.AxluStore.dispatch("product.bulkSetCategory", { ids, categoryId: payload }); break;
      case "reprice":   window.AxluStore.dispatch("product.bulkReprice",   { ids }); break;
      case "delete":    res = window.AxluStore.dispatch("product.bulkDelete", { ids }); break;
    }
    if (res && res.ok === false) {
      setBulkOpen(null);
      return;
    }
    const msg = {
      approve:   `${ids.length} produit(s) approuvé(s)`,
      review:    `${ids.length} produit(s) mis en revue`,
      block:     `${ids.length} produit(s) bloqué(s)`,
      archive:   `${ids.length} produit(s) archivé(s)`,
      publish:   res && res.skipped ? `${res.published} publié(s) · ${res.skipped} incomplet(s) → à vérifier` : `${ids.length} produit(s) ajouté(s) à la file de publication`,
      republish: res && res.skipped ? `${res.published} mis à jour · ${res.skipped} incomplet(s) → à vérifier` : `${ids.length} produit(s) en file de mise à jour Shopify`,
      activate:   `${ids.length} produit(s) activé(s)`,
      deactivate: `${ids.length} produit(s) repassé(s) en brouillon`,
      unpublish: `${ids.length} produit(s) dépubliés de Shopify`,
      category:  `${ids.length} produit(s) re-catégorisé(s)`,
      reprice:   `${ids.length} produit(s) re-tarifé(s)`,
      delete:    `${ids.length} produit(s) supprimé(s)`,
    }[action];
    toast(msg, { tone: action === "delete" ? "danger" : "success" });
    setSelected(new Set());
    setBulkOpen(null);
  };

  const setOneStatus = (id, status, label) => {
    window.AxluStore.dispatch("product.setStatus", { id, status });
    toast(label, { tone: "success" });
  };

  const exportCsv = () => {
    const cols = [
      { key: "sku",          label: "SKU" },
      { key: "title",        label: "Titre" },
      { key: "brand",        label: "Marque" },
      { key: "categoryLabel",label: "Catégorie" },
      { key: "supplierName", label: "Fournisseur" },
      { key: "buyPrice",     label: "P. achat",  value: (r) => (r.buyPrice ?? 0).toFixed(2) },
      { key: "sellPrice",    label: "P. vente",  value: (r) => (r.sellPrice ?? 0).toFixed(2) },
      { key: "margin",       label: "Marge",     value: (r) => ((r.sellPrice ?? 0) - (r.buyPrice ?? 0)).toFixed(2) },
      { key: "marginPct",    label: "Marge %",   value: (r) => (r.marginPct ?? 0).toFixed(1) },
      { key: "stock",        label: "Stock" },
      { key: "status",       label: "Statut" },
    ];
    const csv = window.AxluStore.toCSV(filtered, cols);
    window.AxluStore.download(csv, "catalog.csv", "text/csv");
    toast(`${filtered.length} produits exportés`, { tone: "success" });
  };

  const counts = useMemo(() => {
    const c = {};
    Object.keys(PRODUCT_STATUSES).forEach((k) => c[k] = PRODUCTS.filter((p) => p.status === k).length);
    return c;
  }, [v, PRODUCTS]);

  const shopifyCounts = useMemo(() => {
    const c = {};
    Object.keys(SHOPIFY_PUB_DEF).forEach((k) => c[k] = PRODUCTS.filter((p) => p.shopifyPub === k).length);
    return c;
  }, [v, PRODUCTS]);

  // Real Shopify publish for one product (Stage 2). Pushes via productSet,
  // stores the returned Shopify ID, surfaces success / error as a toast.
  const [publishingId, setPublishingId] = useState(null);
  const publishOneToShopify = async (p) => {
    if (publishingId) return;
    setPublishingId(p.id);
    toast(`Publication de ${p.sku} sur Shopify…`, { tone: "info" });
    try {
      const r = await window.AxluStore.shopifyPublish(p.id);
      if (r && r.ok) {
        const sw = r.stock && r.stock.ok === false ? ` · stock non poussé : ${r.stock.error || "erreur"}` : "";
        toast(`${p.sku} publié sur Shopify ✓${sw}`, { tone: sw ? "warning" : "success" });
      } else toast(`Échec ${p.sku} : ${(r && r.error) || "erreur inconnue"}`, { tone: "danger" });
    } catch (e) {
      toast(`Échec ${p.sku} : ${e}`, { tone: "danger" });
    } finally {
      setPublishingId(null);
    }
  };

  const activateOneToShopify = async (p) => {
    if (publishingId) return;
    setPublishingId(p.id);
    toast(`Activation de ${p.sku} sur Shopify…`, { tone: "info" });
    try {
      const r = await window.AxluStore.shopifyActivate(p.id);
      if (r && r.ok) {
        toast(`${p.sku} activé sur Shopify ✓`, { tone: "success" });
      } else toast(`Échec ${p.sku} : ${(r && r.error) || "erreur inconnue"}`, { tone: "danger" });
    } catch (e) {
      toast(`Échec ${p.sku} : ${e}`, { tone: "danger" });
    } finally {
      setPublishingId(null);
    }
  };

  const deactivateOneToShopify = async (p) => {
    if (publishingId) return;
    setPublishingId(p.id);
    toast(`Désactivation de ${p.sku}…`, { tone: "info" });
    try {
      const r = await window.AxluStore.shopifyDeactivate(p.id);
      if (r && r.ok) {
        toast(`${p.sku} repassé en brouillon ✓`, { tone: "success" });
      } else toast(`Échec ${p.sku} : ${(r && r.error) || "erreur inconnue"}`, { tone: "danger" });
    } catch (e) {
      toast(`Échec ${p.sku} : ${e}`, { tone: "danger" });
    } finally {
      setPublishingId(null);
    }
  };

  return (
    <div>
      <LockBanner resource="page:catalog"/>
      <PageHeader
        breadcrumb={[{ label: "Catalogue" }]}
        title="Catalogue produits"
        subtitle={`${fmt.num(PRODUCTS.length)} produits · ${counts.to_review || 0} à vérifier · ${shopifyCounts.active || 0} actifs · ${shopifyCounts.draft || 0} en brouillon · ${shopifyCounts.error || 0} en erreur`}
        actions={
          <>
            <Button variant="secondary" leftIcon="download" onClick={exportCsv}>Exporter</Button>
            <Button variant="secondary" leftIcon="upload" onClick={() => { if (window.AxluStore.amIReadOnly && window.AxluStore.amIReadOnly()) { const b = catalogBlockedBy(); toast("Catalogue en cours d'utilisation par " + ((b && b.userLabel) || "un autre utilisateur") + " — lecture seule", { tone: "danger" }); return; } setCsvOpen(true); }}>Importer CSV</Button>
            <ViewSwitch view={view} setView={setView}/>
          </>
        }
      />

      <div className="p-6">
        {lastImportFilter && (
          <div className="mb-3 surface border rounded-md px-4 py-2.5 flex items-center justify-between accent-bg" style={{ borderColor: "var(--accent)" }}>
            <div className="text-[12.5px] accent-fg">
              {addedSkusSet.size === 0
                ? "Détails du dernier import indisponibles — refaites un import pour voir les nouveaux produits."
                : <>Affichage des <strong>{sorted.length}</strong> nouveau{sorted.length > 1 ? "x" : ""} produit{sorted.length > 1 ? "s" : ""} du dernier import</>}
            </div>
            <Button variant="ghost" onClick={() => setLastImportFilter(false)}>Tout voir</Button>
          </div>
        )}
        {/* Filters */}
        <div className="flex items-center gap-2 mb-3 flex-wrap">
          <Input leftIcon="search" placeholder="SKU ou titre…" value={search} onChange={(e) => setSearch(e.target.value)} className="w-[260px]"/>
          <Select value={statusFilter} onChange={setStatusFilter} options={[
            { value: "all", label: "Tous statuts" },
            ...Object.entries(PRODUCT_STATUSES).map(([k, def]) => ({ value: k, label: `${def.label} (${counts[k] || 0})` })),
          ]}/>
          <Select value={shopifyFilter} onChange={setShopifyFilter} options={[
            { value: "all", label: "Shopify : tous" },
            ...Object.entries(SHOPIFY_PUB_DEF).map(([k, def]) => ({ value: k, label: `${def.label} (${shopifyCounts[k] || 0})` })),
          ]}/>
          <Select value={categoryFilter} onChange={(val) => { setCategoryFilter(val); setSubCategoryFilter("all"); }} options={[
            { value: "all", label: "Toutes catégories" },
            ...CATEGORIES_AXLU.filter((c) => !c.parent).map((c) => ({ value: c.id, label: c.name })),
          ]}/>
          {categoryFilter !== "all" && (
            <Select value={subCategoryFilter} onChange={setSubCategoryFilter} options={[
              { value: "all", label: "Toutes sous-catégories" },
              ...CATEGORIES_AXLU.filter((c) => c.parent === categoryFilter).map((c) => ({ value: c.id, label: c.name })),
            ]}/>
          )}
          <Select value={supplierFilter} onChange={setSupplierFilter} options={[
            { value: "all", label: "Tous fournisseurs" },
            ...SUPPLIERS.map((s) => ({ value: s.id, label: s.name })),
          ]}/>
          <div className="flex items-center gap-1">
            <Input value={priceMin} onChange={(e) => setPriceMin(e.target.value)} placeholder="€ min" className="w-[80px] mono text-[12px]"/>
            <span className="text-subtle">→</span>
            <Input value={priceMax} onChange={(e) => setPriceMax(e.target.value)} placeholder="€ max" className="w-[80px] mono text-[12px]"/>
          </div>
          <div className="flex items-center gap-1">
            <Input value={marginMin} onChange={(e) => setMarginMin(e.target.value)} placeholder="% marge min" className="w-[100px] mono text-[12px]"/>
            <span className="text-subtle">→</span>
            <Input value={marginMax} onChange={(e) => setMarginMax(e.target.value)} placeholder="% marge max" className="w-[100px] mono text-[12px]"/>
          </div>
          <Select value={hasImageFilter} onChange={setHasImageFilter} options={[
            { value: "all", label: "Image : tous" },
            { value: "yes", label: "Avec image" },
            { value: "no", label: "Sans image" },
          ]}/>
          <Select value={stockFilter} onChange={setStockFilter} options={[
            { value: "all", label: "Stock : tous" },
            { value: "in",  label: "En stock" },
            { value: "out", label: "Rupture (stock 0)" },
          ]}/>
          <Button variant="ghost" onClick={() => {
            setSearch(""); setStatusFilter("all"); setCategoryFilter("all"); setSubCategoryFilter("all"); setSupplierFilter("all");
            setShopifyFilter("all"); setHasImageFilter("all"); setStockFilter("all"); setPriceMin(""); setPriceMax(""); setMarginMin(""); setMarginMax(""); setLastImportFilter(false);
          }}>Réinitialiser</Button>
          <div className="flex-1"/>
          <span className="text-[12px] text-muted">{fmt.num(sorted.length)} résultat{sorted.length > 1 ? "s" : ""}</span>
        </div>

        {/* Bulk action bar */}
        {selected.size > 0 && (
          <BulkBar
            count={selected.size}
            onCancel={() => setSelected(new Set())}
            onAction={(action, payload) => {
              if (["approve", "review", "block", "archive", "publish", "republish", "activate", "deactivate", "unpublish", "category", "reprice", "delete"].includes(action)) {
                if (["block", "archive", "unpublish", "category", "reprice", "delete"].includes(action) && !payload) {
                  setBulkOpen({ action, payload });
                } else {
                  applyBulk(action, payload);
                }
              }
            }}
            categories={CATEGORIES_AXLU}
          />
        )}

        {view === "table" && (
          <Card padding="none">
            <table className="axlu-table">
              <thead>
                <tr>
                  <th style={{ width: 32 }}>
                    <Checkbox checked={selected.size > 0 && selected.size === paginated.length}
                              indeterminate={selected.size > 0 && selected.size < paginated.length}
                              onChange={toggleAll}/>
                  </th>
                  <th style={{ width: 52 }}>Image</th>
                  <SortHeader label="SKU" sortKey="sku" sort={sort} toggle={toggle}/>
                  <SortHeader label="Titre" sortKey="title" sort={sort} toggle={toggle}/>
                  <th>Fournisseur</th>
                  <SortHeader label="Catégorie" sortKey="categoryLabel" sort={sort} toggle={toggle}/>
                  <SortHeader label="P. achat" sortKey="buyPrice" sort={sort} toggle={toggle} align="right"/>
                  <SortHeader label="P. vente" sortKey="sellPrice" sort={sort} toggle={toggle} align="right"/>
                  <SortHeader label="Marge" sortKey="marginPct" sort={sort} toggle={toggle} align="right"/>
                  <SortHeader label="Stock" sortKey="stock" sort={sort} toggle={toggle} align="right"/>
                  <SortHeader label="Statut" sortKey="status" sort={sort} toggle={toggle}/>
                  <SortHeader label="Shopify" sortKey="shopifyPub" sort={sort} toggle={toggle}/>
                  <th></th>
                </tr>
              </thead>
              <tbody>
                {paginated.map((p) => (
                  <tr key={p.id}
                      className={`hover-row cursor-pointer ${selected.has(p.id) ? "row-selected" : ""}`}
                      onClick={() => navigate(`#/products/${p.id}`)}>
                    <td onClick={(e) => { e.stopPropagation(); toggleSelect(p.id); }}>
                      <Checkbox checked={selected.has(p.id)} onChange={() => toggleSelect(p.id)}/>
                    </td>
                    <td><ProductThumb sku={p.sku} src={p.imageUrl} hasImage={p.hasImage} size={40}/></td>
                    <td className="mono text-[12px] text-muted">{p.sku}</td>
                    <td>
                      <div className="flex items-center gap-2 min-w-0">
                        <span className="font-medium truncate" style={{ maxWidth: 280 }}>{p.title}</span>
                        {p.duplicate && <Badge tone="warning">Doublon ?</Badge>}
                      </div>
                      <div className="text-[11px] text-subtle truncate">{p.brand}</div>
                    </td>
                    <td className="text-[12.5px] text-muted">{p.supplierName}</td>
                    <td className="text-[12.5px]">{p.categoryLabel}</td>
                    <td className="text-right mono tabular-nums text-muted">{fmt.eur(p.buyPrice)}</td>
                    <td className="text-right mono tabular-nums">{fmt.eur(p.manualPrice ?? p.sellPrice)}</td>
                    <td className="text-right">
                      <span className={`mono tabular-nums ${p.marginPct < 50 ? "text-amber-600" : ""}`}>{fmt.pct(p.marginPct)}</span>
                    </td>
                    <td className="text-right">
                      <span className={`mono tabular-nums ${p.stockUncounted ? "text-emerald-600" : p.stock === 0 ? "text-red-600" : p.stock < 100 ? "text-amber-600" : ""}`}>{stockText(p.stock, p.stockUncounted)}</span>
                    </td>
                    <td><StatusBadge status={p.status} map={PRODUCT_STATUSES}/></td>
                    <td><div className="flex items-center gap-1.5"><ShopifyPresenceBadge pub={p.shopifyPub}/><ShopifyStateBadge pub={p.shopifyPub}/></div></td>
                    <td onClick={(e) => e.stopPropagation()}>
                      <div className="flex items-center gap-1 justify-end">
                      {p.shopifyPub === 'active'
                        ? <Button variant="secondary" size="sm" leftIcon="eyeOff" disabled={!!publishingId} onClick={(e)=>{e.stopPropagation(); deactivateOneToShopify(p);}}>{publishingId===p.id?"…":"Désactiver"}</Button>
                        : <Button variant="secondary" size="sm" leftIcon="check" disabled={!!publishingId} onClick={(e)=>{e.stopPropagation(); activateOneToShopify(p);}}>{publishingId===p.id?"…":"Activer"}</Button>}
                      <Dropdown
                        trigger={<IconButton icon="more" size="sm" onClick={(e) => e.stopPropagation()}/>}
                        align="right"
                        items={[
                          { label: "Ouvrir la fiche", icon: "eye", onClick: () => navigate(`#/products/${p.id}`) },
                          { label: "Approuver", icon: "check", onClick: () => setOneStatus(p.id, "approved", "Produit approuvé") },
                          { label: publishingId === p.id ? "Publication…" : "Publier sur Shopify", icon: "cart", disabled: !!publishingId, onClick: () => publishOneToShopify(p) },
                          { label: "Activer sur Shopify", icon: "check", disabled: !!publishingId, onClick: () => activateOneToShopify(p) },
                          { label: "Désactiver", icon: "eyeOff", disabled: !!publishingId, onClick: () => deactivateOneToShopify(p) },
                          "sep",
                          { label: "Archiver", icon: "trash", onClick: () => setOneStatus(p.id, "archived", "Produit archivé") },
                          { label: "Bloquer", icon: "cross", danger: true, onClick: () => setOneStatus(p.id, "blocked", "Produit bloqué") },
                          "sep",
                          {
                            label: "Supprimer définitivement",
                            icon: "trash",
                            danger: true,
                            disabled: !canDelete,
                            onClick: () => setConfirmDeleteRow(p),
                          },
                        ]}/>
                      </div>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </Card>
        )}

        {view === "gallery" && (
          <CatalogGallery products={paginated} navigate={navigate}
                          selected={selected} toggleSelect={toggleSelect}/>
        )}

        {view === "kanban" && (
          <CatalogKanban products={sorted} navigate={navigate}
                         onMove={(id, status) => {
                           window.AxluStore.dispatch("product.setStatus", { id, status });
                           toast("Statut mis à jour", { tone: "success" });
                         }}
                         onColumnAction={(action, status) => {
                           const ids = sorted.filter((p) => p.status === status).map((p) => p.id);
                           if (ids.length === 0) { toast("Colonne vide", { tone: "info" }); return; }
                           if (action === "approve") {
                             window.AxluStore.dispatch("product.bulkSetStatus", { ids, status: "approved" });
                             toast(`${ids.length} produit(s) approuvé(s)`, { tone: "success" });
                           } else if (action === "block") {
                             window.AxluStore.dispatch("product.bulkSetStatus", { ids, status: "blocked" });
                             toast(`${ids.length} produit(s) bloqué(s)`, { tone: "success" });
                           } else if (action === "export") {
                             const cols = [
                               { key: "sku", label: "SKU" },
                               { key: "title", label: "Titre" },
                               { key: "brand", label: "Marque" },
                               { key: "buyPrice", label: "P. achat" },
                               { key: "sellPrice", label: "P. vente" },
                               { key: "stock", label: "Stock" },
                               { key: "status", label: "Statut" },
                             ];
                             const rows = sorted.filter((p) => p.status === status);
                             window.AxluStore.download(window.AxluStore.toCSV(rows, cols), `catalog-${status}.csv`, "text/csv");
                             toast(`${rows.length} produits exportés`, { tone: "success" });
                           }
                         }}/>
        )}

        {(view === "table" || view === "gallery") && sorted.length > 0 && (
          <Pagination page={page} lastPage={lastPage} setPage={setPage}
                      shown={paginated.length} total={sorted.length}/>
        )}
      </div>

      {/* Bulk confirm modal */}
      <BulkConfirmModal
        open={!!bulkOpen}
        onClose={() => setBulkOpen(null)}
        action={bulkOpen?.action}
        count={selected.size}
        categories={CATEGORIES_AXLU}
        onConfirm={(payload) => applyBulk(bulkOpen.action, payload)}
      />

      {/* Single-row delete confirmation */}
      <Modal open={!!confirmDeleteRow} onClose={() => setConfirmDeleteRow(null)}
             title="Supprimer ce produit ?"
             footer={<>
               <Button variant="ghost" onClick={() => setConfirmDeleteRow(null)}>Annuler</Button>
               <Button variant="danger" leftIcon="trash" onClick={() => {
                 const p = confirmDeleteRow;
                 if (p) {
                   window.AxluStore.dispatch("product.delete", { id: p.id });
                   toast(`Produit ${p.sku} supprimé`, { tone: "danger" });
                 }
                 setConfirmDeleteRow(null);
               }}>Supprimer définitivement</Button>
             </>}>
        {confirmDeleteRow && (
          <div className="space-y-2 text-[13px]">
            <div>Supprimer définitivement <span className="font-medium">{confirmDeleteRow.sku}</span> — {confirmDeleteRow.title} ?</div>
            <div className="text-muted">Cette action est <strong>irréversible</strong>. Pour conserver l'historique, préférez « Archiver ».</div>
          </div>
        )}
      </Modal>

      {/* CSV import wizard */}
      <CsvImportWizard open={csvOpen} onClose={() => setCsvOpen(false)}
                       navigate={navigate}
                       onDone={(report) => {
                         toast(`Import terminé : ${report.added} créés, ${report.errors} erreur(s)`, { tone: "success" });
                       }}/>
    </div>
  );
}

function ViewSwitch({ view, setView }) {
  const opts = [
    { id: "table",   icon: "list",    label: "Liste" },
    { id: "gallery", icon: "grid",    label: "Galerie" },
    { id: "kanban",  icon: "kanban",  label: "Kanban" },
  ];
  return (
    <div className="flex border rounded-md overflow-hidden surface" style={{ borderColor: "var(--border)" }}>
      {opts.map((o, i) => (
        <button key={o.id} onClick={() => setView(o.id)}
                className={`px-2.5 py-1.5 text-[13px] flex items-center gap-1.5 ${i > 0 ? "border-l" : ""} ${view === o.id ? "accent-bg accent-fg" : "text-muted hover:text-app"}`}
                style={{ borderColor: "var(--border)" }}>
          <Icon name={o.icon} size={14}/> {o.label}
        </button>
      ))}
    </div>
  );
}

// Reusable pager — first / previous / page / next / last. Shared by the
// catalog table and gallery views so paging works the same everywhere.
function Pagination({ page, lastPage, setPage, shown, total }) {
  const go = (p) => setPage(Math.min(lastPage, Math.max(1, p)));
  return (
    <div className="mt-3 surface border rounded-md px-4 py-2.5 flex items-center justify-between text-[12px] text-muted"
         style={{ borderColor: "var(--border)" }}>
      <span>Affichage {fmt.num(shown)} sur {fmt.num(total)}</span>
      <div className="flex items-center gap-1">
        <IconButton icon="chevronsLeft"  size="sm" disabled={page <= 1}        onClick={() => go(1)}        title="Première page"/>
        <IconButton icon="chevronLeft"   size="sm" disabled={page <= 1}        onClick={() => go(page - 1)} title="Page précédente"/>
        <span className="px-2">Page {page} / {lastPage}</span>
        <IconButton icon="chevronRight"  size="sm" disabled={page >= lastPage} onClick={() => go(page + 1)} title="Page suivante"/>
        <IconButton icon="chevronsRight" size="sm" disabled={page >= lastPage} onClick={() => go(lastPage)} title="Dernière page"/>
      </div>
    </div>
  );
}

function ShopifyBadge({ pub }) {
  const def = SHOPIFY_PUB_DEF[pub] || SHOPIFY_PUB_DEF.offline;
  return <Badge tone={def.tone} dot={def.dot}>{def.label}</Badge>;
}

// Présence Shopify : le produit existe-t-il sur la boutique ?
function ShopifyPresenceBadge({ pub }) {
  if (pub === "draft" || pub === "active") return <Badge tone="info" dot>Publié</Badge>;
  if (pub === "error") return <Badge tone="danger" dot>Erreur</Badge>;
  return <Badge tone="muted">Hors-ligne</Badge>;
}

// État de publication : actif / brouillon (rien si hors-ligne ou erreur).
function ShopifyStateBadge({ pub }) {
  if (pub === "active") return <Badge tone="success" dot>Actif</Badge>;
  if (pub === "draft") return <Badge tone="warning" dot>Brouillon</Badge>;
  return null;
}

// ─── Bulk action bar ──────────────────────────────────────────────────
function BulkBar({ count, onCancel, onAction, categories }) {
  return (
    <div className="flex items-center gap-2 px-3 py-2 mb-3 rounded-md anim-slide accent-bg border" style={{ borderColor: "var(--accent)" }}>
      <Icon name="check" size={14} className="accent-fg"/>
      <span className="text-[13px] font-medium accent-fg">{count} produit{count > 1 ? "s" : ""} sélectionné{count > 1 ? "s" : ""}</span>
      <div className="flex-1"/>

      <Dropdown
        trigger={<Button variant="secondary" size="sm" leftIcon="check" rightIcon="chevronDown">Statut</Button>}
        align="right" width={200}
        items={[
          { label: "Approuver",     icon: "check",   onClick: () => onAction("approve") },
          { label: "Mettre en revue", icon: "warning", onClick: () => onAction("review") },
          { label: "Bloquer",       icon: "cross",   onClick: () => onAction("block"), danger: true },
        ]}/>

      <Dropdown
        trigger={<Button variant="secondary" size="sm" leftIcon="cart" rightIcon="chevronDown">Shopify</Button>}
        align="right" width={220}
        items={[
          { label: "Publier sur Shopify",   icon: "upload", onClick: () => onAction("publish") },
          { label: "Mettre à jour Shopify", icon: "refresh", onClick: () => onAction("republish") },
          { label: "Activer",               icon: "check",  onClick: () => onAction("activate") },
          { label: "Désactiver",            icon: "eyeOff", onClick: () => onAction("deactivate") },
          { label: "Dépublier",             icon: "eyeOff", onClick: () => onAction("unpublish"), danger: true },
        ]}/>

      <Dropdown
        trigger={<Button variant="secondary" size="sm" leftIcon="more" rightIcon="chevronDown">Plus</Button>}
        align="right" width={260}
        items={[
          { label: "Affecter à une catégorie…", icon: "tag",    onClick: () => onAction("category") },
          { label: "Recalculer les prix…",      icon: "coin",   onClick: () => onAction("reprice") },
          "sep",
          { label: "Archiver",                  icon: "trash",  onClick: () => onAction("archive"), danger: true },
          {
            label: "Supprimer définitivement…",
            icon: "trash",
            danger: true,
            disabled: !window.AxluStore.can("delete"),
            onClick: () => onAction("delete"),
          },
        ]}/>

      <Button variant="ghost" size="sm" onClick={onCancel}>Annuler</Button>
    </div>
  );
}

function BulkConfirmModal({ open, onClose, action, count, categories, onConfirm }) {
  const [category, setCategory] = useState("");

  useEffect(() => {
    if (open) {
      setCategory(categories.filter((c) => c.parent)[0]?.id || "");
    }
  }, [open, categories]);

  if (!action) return null;

  const titles = {
    block:    "Bloquer les produits sélectionnés",
    archive:  "Archiver les produits sélectionnés",
    unpublish: "Dépublier de Shopify",
    category: "Affecter à une catégorie AXLU",
    reprice:  "Recalculer les prix",
    delete:   "Supprimer définitivement",
  };

  const isDanger = ["block", "archive", "unpublish", "delete"].includes(action);
  const confirmLabel = action === "delete" ? `Supprimer ${count} produit${count > 1 ? "s" : ""}` : `Confirmer (${count})`;

  return (
    <Modal open={open} onClose={onClose}
           title={titles[action]}
           footer={<>
             <Button variant="ghost" onClick={onClose}>Annuler</Button>
             <Button variant={isDanger ? "danger" : "primary"}
                     leftIcon={action === "delete" ? "trash" : undefined}
                     onClick={() => onConfirm(action === "category" ? category : true)}>
               {confirmLabel}
             </Button>
           </>}>
      <div className="text-[13px] text-muted space-y-3">
        {action === "block" && <p>Bloquer <span className="text-app font-medium">{count} produits</span>. Ils ne seront plus mis à jour ni publiés tant qu'ils restent dans cet état.</p>}
        {action === "archive" && <p>Archiver <span className="text-app font-medium">{count} produits</span>. Ils seront masqués du catalogue actif mais conservés pour historique.</p>}
        {action === "unpublish" && <p>Retirer <span className="text-app font-medium">{count} produits</span> de la boutique Shopify. Le statut AXLU n'est pas modifié.</p>}
        {action === "delete" && (
          <>
            <p>Supprimer définitivement <span className="text-app font-medium">{count} produit{count > 1 ? "s" : ""}</span> du catalogue.</p>
            <p className="text-red-600 font-medium">Cette action est irréversible. Pour conserver l'historique, préférez « Archiver ».</p>
          </>
        )}
        {action === "category" && (
          <>
            <p>Affecter <span className="text-app font-medium">{count} produits</span> à la catégorie AXLU :</p>
            <Select value={category} onChange={setCategory}
                    options={categories.filter((c) => c.parent).map((c) => ({ value: c.id, label: c.name }))}/>
          </>
        )}
        {action === "reprice" && (
          <>
            <p>Recalculer le prix de vente de <span className="text-app font-medium">{count} produits</span> avec le coefficient de base global et les frais de transport.</p>
            <p className="text-[12px]">Les prix manuels (verrouillés) ne seront pas écrasés. Configurer les paramètres dans <a href="#/pricing" className="accent-fg hover:underline">Coûts &amp; marges</a>.</p>
          </>
        )}
      </div>
    </Modal>
  );
}

// ─── Gallery view ─────────────────────────────────────────────────────
function CatalogGallery({ products, navigate, selected, toggleSelect, selectable = true }) {
  const v = useAxluStore();
  const { PRODUCT_STATUSES } = window.AxluData;
  const sel = selected || new Set();
  return (
    <div className="grid gap-3" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))" }}>
      {products.map((p) => {
        const isSel = selectable && sel.has(p.id);
        return (
          <div key={p.id}
               onClick={() => navigate(`#/products/${p.id}`)}
               className={`group surface border rounded-lg overflow-hidden cursor-pointer transition-all hover:-translate-y-0.5 ${isSel ? "ring-2" : ""}`}
               style={{
                 borderColor: isSel ? "var(--accent)" : "var(--border)",
                 boxShadow: isSel ? "var(--shadow-md)" : "var(--shadow-sm)",
                 ringColor: "var(--accent)",
               }}>
            <div className="relative" style={{ aspectRatio: "1 / 1" }}>
              <ProductThumb sku={p.sku} src={p.imageUrl} hasImage={p.hasImage} size="100%" className="w-full h-full"/>
              {/* Top-left: checkbox */}
              {selectable && (
              <div onClick={(e) => { e.stopPropagation(); toggleSelect && toggleSelect(p.id); }}
                   className={`absolute top-2 left-2 w-6 h-6 rounded-md surface border flex items-center justify-center transition-opacity ${isSel ? "opacity-100" : "opacity-0 group-hover:opacity-100"}`}
                   style={{ borderColor: isSel ? "var(--accent)" : "var(--border)", background: isSel ? "var(--accent)" : "var(--surface)" }}>
                {isSel && <Icon name="check" size={14} className="text-white"/>}
              </div>
              )}
              {/* Top-right: shopify dot */}
              <div className="absolute top-2 right-2"><ShopifyBadge pub={p.shopifyPub}/></div>
              {/* Bottom-right: stock if low */}
              {p.stock === 0 && !p.stockUncounted && (
                <div className="absolute bottom-2 right-2"><Badge tone="danger">Rupture</Badge></div>
              )}
              {p.duplicate && (
                <div className="absolute bottom-2 left-2"><Badge tone="warning">Doublon ?</Badge></div>
              )}
            </div>
            <div className="p-2.5 space-y-1">
              <div className="text-[12.5px] font-medium leading-tight line-clamp-2" style={{ minHeight: 32 }}>{p.title}</div>
              <div className="flex items-center justify-between text-[11px]">
                <span className="mono text-subtle">{p.sku}</span>
                <StatusBadge status={p.status} map={PRODUCT_STATUSES}/>
              </div>
              <div className="flex items-center justify-between pt-1 border-t" style={{ borderColor: "var(--border)" }}>
                <span className="mono tabular-nums text-[13px] font-semibold">{fmt.eur(p.sellPrice)}</span>
                <span className={`text-[11px] mono ${p.marginPct < 50 ? "text-amber-600" : "text-muted"}`}>marge {fmt.pct(p.marginPct)}</span>
              </div>
            </div>
          </div>
        );
      })}
      {products.length === 0 && (
        <div className="col-span-full">
          <EmptyState icon="cube" title="Aucun produit" description="Ajustez les filtres pour afficher des résultats."/>
        </div>
      )}
    </div>
  );
}

// ─── Kanban ────────────────────────────────────────────────────────────
function CatalogKanban({ products, navigate, onMove, onColumnAction }) {
  const { PRODUCT_STATUSES } = window.AxluData;
  const cols = ["to_review", "approved", "blocked"];
  const [dragId, setDragId] = useState(null);
  // Per-column "shown" count: starts at 12, grows by 100 each click on the
  // « + X autres » button. Kept local so navigating away and back resets it.
  const [shown, setShown] = useState({});
  const INITIAL = 12;
  const STEP = 100;
  const showMore = (col) => setShown((s) => ({ ...s, [col]: (s[col] || INITIAL) + STEP }));
  return (
    <div className="grid gap-3 overflow-x-auto" style={{ gridTemplateColumns: `repeat(${cols.length}, minmax(260px, 1fr))` }}>
      {cols.map((col) => {
        const items = products.filter((p) => p.status === col);
        const limit = Math.min(shown[col] || INITIAL, items.length);
        const remaining = items.length - limit;
        return (
          <div key={col}
               className="surface border rounded-lg flex flex-col"
               style={{ borderColor: "var(--border)", minHeight: 400 }}
               onDragOver={(e) => e.preventDefault()}
               onDrop={() => { if (dragId) { onMove(dragId, col); setDragId(null); } }}>
            <div className="px-3 py-2.5 border-b flex items-center justify-between" style={{ borderColor: "var(--border)" }}>
              <div className="flex items-center gap-2">
                <StatusBadge status={col} map={PRODUCT_STATUSES}/>
                <span className="text-[12px] text-muted mono tabular-nums">{items.length}</span>
              </div>
              <Dropdown
                trigger={<IconButton icon="more" size="sm"/>}
                align="right" width={220}
                items={[
                  { label: "Approuver toute la colonne", icon: "check",  onClick: () => onColumnAction && onColumnAction("approve", col) },
                  { label: "Bloquer toute la colonne",   icon: "cross",  onClick: () => onColumnAction && onColumnAction("block", col),  danger: true },
                  "sep",
                  { label: "Exporter colonne CSV",       icon: "download", onClick: () => onColumnAction && onColumnAction("export", col) },
                ]}/>
            </div>
            <div className="p-2 flex-1 overflow-y-auto space-y-2" style={{ maxHeight: 600 }}>
              {items.slice(0, limit).map((p) => (
                <div key={p.id}
                     draggable
                     onDragStart={() => setDragId(p.id)}
                     onClick={() => navigate(`#/products/${p.id}`)}
                     className="surface border rounded-md p-2.5 cursor-pointer hover:bg-[var(--hover)] transition-colors"
                     style={{ borderColor: "var(--border)" }}>
                  <div className="flex items-start gap-2">
                    <ProductThumb sku={p.sku} src={p.imageUrl} hasImage={p.hasImage} size={36}/>
                    <div className="flex-1 min-w-0">
                      <div className="text-[12.5px] font-medium leading-tight line-clamp-2">{p.title}</div>
                      <div className="text-[11px] text-subtle mono mt-1">{p.sku}</div>
                    </div>
                  </div>
                  <div className="flex items-center justify-between mt-2 text-[11px]">
                    <span className="mono tabular-nums">{fmt.eur(p.sellPrice)}</span>
                    <ShopifyBadge pub={p.shopifyPub}/>
                  </div>
                </div>
              ))}
              {remaining > 0 && (
                <button type="button"
                        onClick={() => showMore(col)}
                        className="w-full text-[11.5px] accent-fg text-center py-1.5 rounded-md hover:bg-[var(--hover)] transition-colors font-medium">
                  + {remaining} autre{remaining > 1 ? "s" : ""} — voir {Math.min(STEP, remaining)} de plus
                </button>
              )}
              {items.length === 0 && (
                <div className="text-[11px] text-subtle text-center py-6">Glisser-déposer ici</div>
              )}
            </div>
          </div>
        );
      })}
    </div>
  );
}

// ─── CSV import wizard ────────────────────────────────────────────────
const CSV_SAMPLE_COLUMNS = ["sku", "title", "category", "brand", "buy_price", "stock", "weight_g", "ean", "description"];
const CSV_SAMPLE_ROWS = [
  ["DEMO-001", "Article démo A", "office", "—", "1.20", "100", "50", "0000000000000", "Exemple de description"],
  ["DEMO-002", "Article démo B", "writing", "—", "0.85", "250", "20", "0000000000017", "Exemple de description"],
];

const AXLU_FIELDS = [
  { value: "",            label: "— Ignorer —",      required: false },
  { value: "sku",         label: "SKU fournisseur",  required: true },
  { value: "title",       label: "Titre produit",    required: true },
  { value: "categoryId",  label: "Catégorie (code)" },
  { value: "brand",       label: "Marque" },
  { value: "buyPrice",    label: "Prix d'achat",     required: true },
  { value: "stock",       label: "Stock" },
  { value: "weight",      label: "Poids (g)" },
  { value: "ean",         label: "EAN/GTIN" },
  { value: "description", label: "Description" },
];

function parseCsvText(text) {
  if (!text) return { columns: [], rows: [] };
  const firstLine = text.split(/\r?\n/, 1)[0] || "";
  const sep = firstLine.includes(";") && (!firstLine.includes(",") || firstLine.split(";").length > firstLine.split(",").length) ? ";" : ",";
  const parseLine = (line) => {
    const out = [];
    let cur = "";
    let inQ = false;
    for (let i = 0; i < line.length; i++) {
      const ch = line[i];
      if (inQ) {
        if (ch === '"' && line[i + 1] === '"') { cur += '"'; i++; }
        else if (ch === '"') { inQ = false; }
        else cur += ch;
      } else {
        if (ch === '"') inQ = true;
        else if (ch === sep) { out.push(cur); cur = ""; }
        else cur += ch;
      }
    }
    out.push(cur);
    return out;
  };
  const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
  if (lines.length === 0) return { columns: [], rows: [] };
  const columns = parseLine(lines[0]).map((c) => c.trim());
  const rows = lines.slice(1).map(parseLine);
  return { columns, rows };
}

function CsvImportWizard({ open, onClose, onDone, navigate }) {
  const [step, setStep] = useState(1); // 1 upload, 2 mapping, 3 preview, 4 report
  const [mapping, setMapping] = useState({});
  const [overwrite, setOverwrite] = useState(false);
  const [defaultStatus, setDefaultStatus] = useState("to_review");
  const [columns, setColumns] = useState([]);
  const [rows, setRows] = useState([]);
  const [fileName, setFileName] = useState("");
  const [importReport, setImportReport] = useState(null);
  const fileRef = useRef(null);
  const toast = useToast();

  const buildAutoMapping = (cols) => {
    const guess = (c) => {
      const k = c.toLowerCase();
      if (k.includes("sku") || k.includes("code")) return "sku";
      if (k.includes("name") || k.includes("title") || k.includes("titre") || k.includes("libelle")) return "title";
      if (k.includes("categ")) return "categoryId";
      if (k.includes("brand") || k.includes("marque")) return "brand";
      if (k.includes("buy") || k.includes("achat") || k.includes("cost")) return "buyPrice";
      if (k.includes("stock") || k.includes("qty") || k.includes("quantite")) return "stock";
      if (k.includes("weight") || k.includes("poids")) return "weight";
      if (k.includes("ean") || k.includes("gtin")) return "ean";
      if (k.includes("desc")) return "description";
      return "";
    };
    const m = {};
    cols.forEach((c) => { m[c] = guess(c); });
    return m;
  };

  useEffect(() => {
    if (open) {
      setStep(1);
      setColumns([]);
      setRows([]);
      setFileName("");
      setImportReport(null);
      setMapping({});
      setOverwrite(false);
      setDefaultStatus("to_review");
    }
  }, [open]);

  const onFilePicked = (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => {
      const text = String(reader.result || "");
      const parsed = parseCsvText(text);
      if (parsed.columns.length === 0) { toast("Fichier CSV vide ou illisible", { tone: "danger" }); return; }
      setColumns(parsed.columns);
      setRows(parsed.rows);
      setFileName(file.name);
      setMapping(buildAutoMapping(parsed.columns));
      toast(`${parsed.rows.length} lignes lues`, { tone: "success" });
      setStep(2);
    };
    reader.readAsText(file, "utf-8");
    e.target.value = "";
  };

  const loadSample = () => {
    setColumns(CSV_SAMPLE_COLUMNS);
    setRows(CSV_SAMPLE_ROWS);
    setFileName("echantillon_demo.csv");
    setMapping(buildAutoMapping(CSV_SAMPLE_COLUMNS));
    toast("Échantillon chargé", { tone: "info" });
    setStep(2);
  };

  const required = ["sku", "title", "buyPrice"];
  const mapped = Object.values(mapping).filter(Boolean);
  const missingRequired = required.filter((r) => !mapped.includes(r));
  const canPreview = missingRequired.length === 0;

  const validation = useMemo(() => {
    const colIdx = {};
    Object.entries(mapping).forEach(([col, field]) => { if (field) colIdx[field] = columns.indexOf(col); });
    return rows.map((row, i) => {
      const errs = [];
      if (colIdx.sku == null || !row[colIdx.sku]) errs.push("SKU manquant");
      if (colIdx.title == null || !row[colIdx.title]) errs.push("Titre manquant");
      const buy = parseFloat(row[colIdx.buyPrice]);
      if (isNaN(buy)) errs.push("Prix d'achat invalide");
      return { row, errs, idx: i };
    });
  }, [mapping, columns, rows]);

  const validCount  = validation.filter((v) => v.errs.length === 0).length;
  const errorCount  = validation.filter((v) => v.errs.length > 0).length;

  const buildItems = () => {
    const colIdx = {};
    Object.entries(mapping).forEach(([col, field]) => { if (field) colIdx[field] = columns.indexOf(col); });
    return validation.filter((v) => v.errs.length === 0).map((v) => {
      const item = { status: defaultStatus };
      Object.entries(colIdx).forEach(([field, idx]) => {
        if (idx < 0) return;
        let val = v.row[idx];
        if (field === "buyPrice" || field === "stock" || field === "weight") {
          const n = parseFloat(val);
          val = isNaN(n) ? 0 : n;
        }
        item[field] = val;
      });
      return item;
    });
  };

  const runImport = () => {
    const items = buildItems();
    window.AxluStore.dispatch("product.import", { items });
    const rep = { added: items.length, errors: errorCount, duration: 800 };
    setImportReport(rep);
    setStep(4);
  };

  const goToCatalog = () => {
    if (importReport) onDone(importReport);
    onClose();
    if (navigate) navigate("#/catalog");
    else window.location.hash = "#/catalog";
  };

  const report = importReport || {
    added: validCount,
    errors: errorCount,
    duration: 800,
    created: validCount,
    updated: 0,
    skipped: 0,
  };

  return (
    <Modal open={open} onClose={onClose} width={840}
           title={
             <div className="flex items-center gap-3">
               <span>Importer un CSV</span>
               <CsvSteps step={step}/>
             </div>
           }
           footer={
             step === 1 ? (
               <>
                 <Button variant="ghost" onClick={onClose}>Annuler</Button>
                 <Button variant="primary" rightIcon="arrowRight" onClick={() => setStep(2)} disabled={columns.length === 0}>Étape suivante</Button>
               </>
             ) : step === 2 ? (
               <>
                 <Button variant="ghost" onClick={() => setStep(1)}>Retour</Button>
                 <div className="flex-1"/>
                 <Button variant="primary" rightIcon="arrowRight" disabled={!canPreview} onClick={() => setStep(3)}>
                   Aperçu des données
                 </Button>
               </>
             ) : step === 3 ? (
               <>
                 <Button variant="ghost" onClick={() => setStep(2)}>Retour</Button>
                 <div className="flex-1"/>
                 <Button variant="primary" leftIcon="upload" onClick={runImport}>
                   Lancer l'import ({validCount} lignes)
                 </Button>
               </>
             ) : (
               <>
                 <Button variant="ghost" onClick={() => { if (importReport) onDone(importReport); onClose(); }}>Fermer</Button>
                 <Button variant="primary" leftIcon="catalog" onClick={goToCatalog}>
                   Voir les produits importés
                 </Button>
               </>
             )
           }>
        {/* Step 1: upload */}
        {step === 1 && (
          <div className="space-y-4">
            <input type="file" accept=".csv,text/csv" hidden ref={fileRef} onChange={onFilePicked}/>
            <div className="border-2 border-dashed rounded-lg p-8 text-center sunken" style={{ borderColor: "var(--border-strong)" }}>
              <Icon name="upload" size={28} className="mx-auto text-subtle mb-3"/>
              <div className="text-[14px] font-medium mb-1">Glisser-déposer un fichier CSV</div>
              <div className="text-[12px] text-muted mb-3">ou</div>
              <div className="flex items-center justify-center gap-2">
                <Button variant="secondary" leftIcon="upload" onClick={() => fileRef.current && fileRef.current.click()}>Parcourir…</Button>
                <Button variant="ghost" leftIcon="file" onClick={loadSample}>Charger un échantillon</Button>
              </div>
              <div className="text-[11px] text-subtle mt-3">UTF-8 recommandé, séparateurs , ; ou tab. Taille max 50 Mo.</div>
            </div>

            {fileName && columns.length > 0 && (
              <div className="surface border rounded-md p-3" style={{ borderColor: "var(--border)" }}>
                <div className="flex items-center gap-2 mb-2">
                  <Icon name="info" size={14} className="accent-fg"/>
                  <span className="text-[12.5px] font-medium">Fichier sélectionné</span>
                </div>
                <div className="flex items-center gap-3">
                  <Icon name="log" size={20} className="text-muted"/>
                  <div className="flex-1 min-w-0">
                    <div className="text-[13px] font-medium">{fileName}</div>
                    <div className="text-[11px] text-subtle mono">{rows.length} lignes · {columns.length} colonnes · UTF-8</div>
                  </div>
                  <IconButton icon="x" size="sm" onClick={() => { setColumns([]); setRows([]); setFileName(""); setMapping({}); }}/>
                </div>
              </div>
            )}

            <Field label="Comportement en cas de SKU déjà existant">
              <div className="flex gap-2">
                <button onClick={() => setOverwrite(false)}
                        className={`flex-1 surface border rounded-md p-2.5 text-left ${!overwrite ? "border-2" : ""}`}
                        style={{ borderColor: !overwrite ? "var(--accent)" : "var(--border)" }}>
                  <div className="text-[12.5px] font-medium">Ignorer</div>
                  <div className="text-[11px] text-subtle">Ne pas modifier les produits existants</div>
                </button>
                <button onClick={() => setOverwrite(true)}
                        className={`flex-1 surface border rounded-md p-2.5 text-left ${overwrite ? "border-2" : ""}`}
                        style={{ borderColor: overwrite ? "var(--accent)" : "var(--border)" }}>
                  <div className="text-[12.5px] font-medium">Mettre à jour</div>
                  <div className="text-[11px] text-subtle">Écraser les champs mappés</div>
                </button>
              </div>
            </Field>
            <Field label="Statut par défaut des nouveaux produits">
              <Select value={defaultStatus} onChange={setDefaultStatus} options={[
                { value: "to_review", label: "À vérifier" },
                { value: "approved",  label: "Approuvé directement" },
              ]}/>
            </Field>
          </div>
        )}

        {/* Step 2: column mapping */}
        {step === 2 && (
          <div className="space-y-3">
            <div className="text-[12.5px] text-muted">
              Associer chaque colonne du CSV à un champ AXLU. Les champs marqués <span className="text-red-600">*</span> sont obligatoires.
            </div>
            <div className="surface border rounded-md overflow-hidden" style={{ borderColor: "var(--border)" }}>
              <table className="axlu-table">
                <thead>
                  <tr>
                    <th>Colonne CSV</th>
                    <th>Aperçu (3 premières valeurs)</th>
                    <th style={{ width: 240 }}>Champ AXLU</th>
                  </tr>
                </thead>
                <tbody>
                  {columns.map((col) => {
                    const samples = rows.slice(0, 3).map((r) => r[columns.indexOf(col)]);
                    return (
                      <tr key={col}>
                        <td className="mono text-[12px]">{col}</td>
                        <td className="text-[12px] text-subtle">
                          {samples.filter(Boolean).slice(0, 3).map((s, i) => (
                            <span key={i} className="inline-block mr-2 px-1.5 py-0.5 rounded sunken truncate" style={{ maxWidth: 140 }}>{s}</span>
                          ))}
                        </td>
                        <td>
                          <Select size="sm" value={mapping[col] || ""}
                                  onChange={(val) => setMapping((m) => ({ ...m, [col]: val }))}
                                  options={AXLU_FIELDS.map((f) => ({ value: f.value, label: f.label + (f.required ? " *" : "") }))}/>
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
            {missingRequired.length > 0 ? (
              <div className="flex items-center gap-2 px-3 py-2 rounded-md border" style={{ background: "rgba(217,119,6,.08)", borderColor: "rgb(217,119,6)" }}>
                <Icon name="warning" size={14} className="text-amber-600"/>
                <span className="text-[12.5px] text-amber-700 dark:text-amber-400">
                  Champs requis manquants : {missingRequired.join(", ")}
                </span>
              </div>
            ) : (
              <div className="flex items-center gap-2 px-3 py-2 rounded-md border" style={{ background: "rgba(16,185,129,.08)", borderColor: "rgb(16,185,129)" }}>
                <Icon name="check2" size={14} className="text-emerald-600"/>
                <span className="text-[12.5px] text-emerald-700 dark:text-emerald-400">
                  {mapped.length} champ{mapped.length > 1 ? "s" : ""} mappé{mapped.length > 1 ? "s" : ""}, prêt pour l'aperçu.
                </span>
              </div>
            )}
          </div>
        )}

        {/* Step 3: preview */}
        {step === 3 && (
          <div className="space-y-3">
            <div className="grid grid-cols-3 gap-3">
              <Stat label="Lignes valides" value={validCount} tone="success"/>
              <Stat label="Avec erreurs" value={errorCount} tone={errorCount > 0 ? "danger" : "neutral"}/>
              <Stat label="Total" value={validCount + errorCount}/>
            </div>
            <div className="surface border rounded-md overflow-hidden" style={{ borderColor: "var(--border)" }}>
              <table className="axlu-table">
                <thead>
                  <tr>
                    <th style={{ width: 36 }}></th>
                    <th>SKU</th><th>Titre</th><th>Catégorie</th>
                    <th className="text-right">P. achat</th>
                    <th className="text-right">Stock</th>
                    <th>Erreurs</th>
                  </tr>
                </thead>
                <tbody>
                  {validation.map((v) => (
                    <tr key={v.idx} className={v.errs.length ? "bg-red-50/40 dark:bg-red-950/20" : ""}>
                      <td>
                        {v.errs.length === 0
                          ? <Icon name="check2" size={16} className="text-emerald-600"/>
                          : <Icon name="cross"  size={16} className="text-red-600"/>}
                      </td>
                      <td className="mono text-[12px]">{v.row[0] || <span className="text-red-600">—</span>}</td>
                      <td>{v.row[1] || <span className="text-red-600">—</span>}</td>
                      <td className="mono text-[11px]">{v.row[2] || <span className="text-subtle">—</span>}</td>
                      <td className="text-right mono tabular-nums">
                        {isNaN(parseFloat(v.row[4]))
                          ? <span className="text-red-600">{v.row[4] || "—"}</span>
                          : fmt.eur(parseFloat(v.row[4]))}
                      </td>
                      <td className="text-right mono tabular-nums">{v.row[5]}</td>
                      <td className="text-[11px] text-red-600">
                        {v.errs.length ? v.errs.join(", ") : "—"}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
            <div className="text-[11px] text-subtle">
              {errorCount > 0 ? `Les ${errorCount} ligne(s) en erreur seront ignorées.` : "Toutes les lignes seront importées."}
              {" "}Comportement doublons : <span className="text-app font-medium">{overwrite ? "mise à jour" : "ignorer"}</span>.
            </div>
          </div>
        )}

        {/* Step 4: report */}
        {step === 4 && (
          <div className="space-y-4">
            <div className="text-center py-6">
              <div className="w-14 h-14 rounded-full bg-emerald-100 dark:bg-emerald-950/50 flex items-center justify-center mx-auto mb-3">
                <Icon name="check2" size={28} className="text-emerald-600"/>
              </div>
              <div className="text-[16px] font-semibold mb-1">Import terminé</div>
              <div className="text-[12.5px] text-muted">{rows.length} lignes traitées en {((report.duration || 800) / 1000).toFixed(1).replace(".", ",")} s</div>
            </div>
            <div className="grid grid-cols-3 gap-3">
              <Stat label="Créés"   value={fmt.num(report.added || 0)} tone="success"/>
              <Stat label="Ignorés" value={fmt.num(errorCount)} tone="neutral"/>
              <Stat label="Erreurs" value={fmt.num(report.errors || 0)} tone={(report.errors || 0) > 0 ? "danger" : "neutral"}/>
            </div>
            {(report.errors || 0) > 0 && (
              <Card title="Erreurs (extrait)" padding="none">
                <table className="axlu-table">
                  <thead><tr><th>Ligne</th><th>SKU</th><th>Erreur</th></tr></thead>
                  <tbody>
                    {validation.filter((v) => v.errs.length > 0).map((v) => (
                      <tr key={v.idx}>
                        <td className="mono text-[12px] text-muted">L{v.idx + 2}</td>
                        <td className="mono text-[12px]">{v.row[0] || "—"}</td>
                        <td className="text-[12px] text-red-600">{v.errs.join(", ")}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </Card>
            )}
            <div className="flex justify-end">
              <Button variant="ghost" size="sm" leftIcon="download" onClick={() => {
                const txt = ["Ligne,SKU,Erreurs", ...validation.filter((v) => v.errs.length).map((v) => `L${v.idx + 2},${v.row[0] || ""},"${v.errs.join("; ")}"`)].join("\n");
                window.AxluStore.download(txt || "Aucune erreur", "import-report.csv", "text/csv");
              }}>Télécharger le rapport complet</Button>
            </div>
          </div>
        )}
    </Modal>
  );
}

function CsvSteps({ step }) {
  const steps = ["Fichier", "Mapping", "Aperçu", "Rapport"];
  return (
    <div className="flex items-center gap-1.5 text-[11px]">
      {steps.map((s, i) => (
        <Fragment key={s}>
          <span className={`px-1.5 py-0.5 rounded ${i + 1 === step ? "accent-bg accent-fg font-medium" : i + 1 < step ? "text-emerald-600" : "text-subtle"}`}>
            {i + 1 < step ? "✓ " : ""}{s}
          </span>
          {i < steps.length - 1 && <span className="text-subtle">·</span>}
        </Fragment>
      ))}
    </div>
  );
}

Object.assign(window, { SyncsList, SyncDetail, CatalogList, ShopifyBadge, ShopifyPresenceBadge, ShopifyStateBadge, SHOPIFY_PUB_DEF });
