// AXLU — screens 1: Login, Dashboard, Suppliers (list + detail)

// ─── Login ─────────────────────────────────────────────────────────────
function LoginScreen({ onLogin }) {
  const [mode, setMode] = useState("password"); // "password" | "key"
  const [email, setEmail] = useState("");
  const [pwd, setPwd] = useState("");
  const [key, setKey] = useState("");
  const [showPwd, setShowPwd] = useState(false);
  const [remember, setRemember] = useState(true);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [forgotOpen, setForgotOpen] = useState(false);

  const submit = (e) => {
    e.preventDefault();
    setError(null);
    setLoading(true);
    // Capture password before clearing it
    const pwdSnapshot = pwd;
    const keySnapshot = key;
    // Clear sensitive fields immediately from state (never show on screen)
    if (mode === "password") setPwd("");
    else setKey("");
    setTimeout(async () => {
      const action = mode === "password" ? "auth.login" : "auth.loginWithKey";
      const payload = mode === "password"
        ? { email: email.trim(), password: pwdSnapshot, rememberMe: remember }
        : { email: email.trim(), key: keySnapshot.trim().toUpperCase(), rememberMe: remember };
      const res = (await window.AxluStore.dispatch(action, payload)) || {};
      setLoading(false);
      if (!res.ok) {
        setError(res.error || "Échec de la connexion");
        return;
      }
      // The forced "must change password" modal is rendered at the App level
      // (so it isn't unmounted with this screen). Just signal the login.
      onLogin();
    }, 350);
  };

  return (
    <div className="min-h-screen flex" style={{ background: "var(--bg)" }}>
      <div className="flex-1 flex items-center justify-center px-6">
        <form onSubmit={submit} className="w-full max-w-[380px]">
          <div className="mb-8 flex items-center gap-3">
            <Logo size={48}/>
            <div>
              <div className="text-[12px] text-muted">Middleware ERP · Luxembourg</div>
            </div>
          </div>
          <h1 className="text-[22px] font-semibold tracking-tight mb-1.5">Connexion</h1>
          <div className="text-[13px] text-muted mb-6">Espace opérateur. Accès réservé.</div>

          <div className="flex p-0.5 mb-5 rounded-md sunken text-[12px] border" style={{ borderColor: "var(--border)" }}>
            <button type="button" onClick={() => { setMode("password"); setError(null); }}
              className={`flex-1 py-1.5 rounded ${mode === "password" ? "surface font-medium" : "text-muted"}`}>
              Mot de passe
            </button>
            <button type="button" onClick={() => { setMode("key"); setError(null); }}
              className={`flex-1 py-1.5 rounded ${mode === "key" ? "surface font-medium" : "text-muted"}`}>
              Clé de connexion
            </button>
          </div>

          <label className="block text-[12px] font-medium text-muted mb-1.5">Email</label>
          <Input value={email} onChange={(e) => setEmail(e.target.value)} leftIcon="users" size="lg" className="mb-3" autoFocus
                 placeholder="vous@axlu.lu"/>

          {mode === "password" ? (
            <>
              <div className="flex items-center justify-between mb-1.5">
                <label className="block text-[12px] font-medium text-muted">Mot de passe</label>
                <a href="#" onClick={(e) => { e.preventDefault(); setForgotOpen(true); }} className="text-[12px] accent-fg hover:underline">Mot de passe oublié ?</a>
              </div>
              <Input type={showPwd ? "text" : "password"} value={pwd} onChange={(e) => setPwd(e.target.value)} size="lg"
                     autoComplete="current-password" spellCheck={false}
                     rightSlot={<IconButton icon={showPwd ? "eyeOff" : "eye"} size="sm" onClick={() => setShowPwd((s) => !s)}/>}
                     className="mb-3"/>
            </>
          ) : (
            <>
              <label className="block text-[12px] font-medium text-muted mb-1.5">Clé de connexion</label>
              <Input value={key} onChange={(e) => setKey(e.target.value.toUpperCase())} size="lg"
                     placeholder="XXXX-XXXX-XXXX" className="mb-3 mono"/>
              <div className="text-[11px] text-subtle mb-3">
                Clé à usage unique, communiquée par un administrateur. Vous serez invité à définir un mot de passe après connexion.
              </div>
            </>
          )}

          <label className="flex items-center gap-2 mb-4 text-[12.5px] text-muted cursor-pointer select-none">
            <input type="checkbox" checked={remember} onChange={(e) => setRemember(e.target.checked)}
                   className="h-3.5 w-3.5"/>
            Se souvenir de moi sur cet ordinateur
          </label>

          {error && (
            <div className="mb-3 px-3 py-2 rounded text-[12.5px]"
                 style={{ background: "rgba(239,68,68,.10)", color: "#b91c1c", border: "1px solid rgba(239,68,68,.25)" }}>
              {error}
            </div>
          )}

          <Button type="submit" variant="primary" size="lg" className="w-full"
                  disabled={loading || !email.trim() || (mode === "password" ? !pwd : !key.trim())}>
            {loading ? "Connexion…" : "Se connecter"}
          </Button>

          <div className="mt-6 pt-5 border-t text-[12px] text-subtle text-center" style={{ borderColor: "var(--border)" }}>
            Connexion sécurisée · SSO Luxembourg disponible bientôt
          </div>
        </form>
      </div>

      <Modal open={forgotOpen} onClose={() => setForgotOpen(false)} title="Mot de passe oublié" width={460}
             footer={<Button variant="primary" onClick={() => setForgotOpen(false)}>J'ai compris</Button>}>
        <div className="text-[13px] text-muted leading-relaxed space-y-3">
          <p>Pour réinitialiser un mot de passe, contactez un administrateur AXLU. Il pourra vous générer une <strong>clé de connexion à usage unique</strong> depuis l'espace utilisateurs.</p>
          <p>Connectez-vous ensuite via l'onglet <strong>« Clé de connexion »</strong> ; vous serez invité à définir un nouveau mot de passe.</p>
        </div>
      </Modal>

    </div>
  );
}

function Logo({ size = 24 }) {
  return (
    <img src="assets/axlu-logo.png" alt="AXLU" className="shrink-0 select-none"
         style={{ height: size, width: "auto", display: "block" }} draggable={false}/>
  );
}

function PwdRule({ ok, text }) {
  return (
    <div className="flex items-center gap-2" style={{ color: ok ? "#059669" : "var(--fg-subtle)" }}>
      <Icon name={ok ? "check2" : "info"} size={13}/>
      <span>{text}</span>
    </div>
  );
}

// ─── Dashboard ─────────────────────────────────────────────────────────
// Dashboard alerts the user has acknowledged — persisted by title so a
// changed situation (e.g. a different count) resurfaces as a fresh alert.
const ALERT_DISMISS_KEY = "axlu.dashboard.dismissedAlerts";
function loadDismissedAlerts() {
  try {
    const a = JSON.parse(localStorage.getItem(ALERT_DISMISS_KEY) || "[]");
    return Array.isArray(a) ? a : [];
  } catch (e) { return []; }
}
function saveDismissedAlerts(list) {
  try { localStorage.setItem(ALERT_DISMISS_KEY, JSON.stringify(list)); } catch (e) { /* ignore */ }
}

function Dashboard({ navigate }) {
  const v = useAxluStore();
  const { PRODUCTS, SUPPLIERS, SYNCS, AUDIT_LOG, PRODUCT_STATUSES, SYNC_STATUSES, CATEGORIES_PF, MARKING_GRIDS, PUBLICATIONS_QUEUE } = window.AxluData;
  const toast = useToast();
  const [refreshing, setRefreshing] = useState(false);
  const [dismissed, setDismissed] = useState(loadDismissedAlerts);
  const counts = {
    imported: PRODUCTS.length,
    toReview: PRODUCTS.filter((p) => p.status === "to_review").length,
    published: PRODUCTS.filter((p) => p.shopifyPub === "active" || p.shopifyPub === "draft").length,
    errors: PRODUCTS.filter((p) => p.shopifyError).length,
  };

  // Build dynamic alerts from real data — no hardcoded scenarios.
  const alerts = [];
  SUPPLIERS.forEach((s) => {
    if (supplierStatus(s) === "unconfigured") {
      alerts.push({ tone: "info", icon: "info", title: `${s.name} — aucun flux configuré`, desc: "Renseignez les URL de flux pour activer la synchronisation.", action: "Configurer", href: `#/suppliers/${s.id}` });
    }
  });
  const unmappedCats = (CATEGORIES_PF || []).filter((c) => !c.mappedTo).length;
  if (unmappedCats > 0) {
    alerts.push({ tone: "info", icon: "info", title: `Mapping incomplet : ${unmappedCats} catégorie${unmappedCats > 1 ? "s" : ""} non assignée${unmappedCats > 1 ? "s" : ""}`, desc: "Compléter le mapping pour intégrer ces produits au catalogue.", action: "Mapper", href: "#/categories?filter=unmapped" });
  }
  const lowMargin = PRODUCTS.filter((p) => p.marginPct != null && p.marginPct < 30).length;
  if (lowMargin > 0) {
    alerts.push({ tone: "warning", icon: "warning", title: `${lowMargin} produit${lowMargin > 1 ? "s" : ""} à marge anormale (< 30%)`, desc: "Ouvre la liste filtrée de ces produits dans le catalogue.", action: "Voir les produits", href: "#/catalog?marginMax=30" });
  }

  // Import-change alerts — surfaced from the last feed import (LAST_IMPORT).
  const lastImport = window.AxluData.LAST_IMPORT;
  // Only surface this alert when we can actually deliver on its promise —
  // i.e. when we have the exact SKUs to filter the catalog on. An older
  // LAST_IMPORT (pre-0.30.3) lacks addedSkus → click would land on 0 result.
  if (lastImport && lastImport.added > 0 && Array.isArray(lastImport.addedSkus) && lastImport.addedSkus.length > 0) {
    alerts.push({ tone: "info", icon: "info", title: `${lastImport.added} nouveau${lastImport.added > 1 ? "x" : ""} produit${lastImport.added > 1 ? "s" : ""} importé${lastImport.added > 1 ? "s" : ""}`, desc: "Ajoutés au dernier import fournisseur — à contrôler puis publier.", action: "Voir", href: "#/catalog?lastImport=1" });
  }
  if (lastImport && lastImport.reverted > 0) {
    alerts.push({ tone: "warning", icon: "warning", title: `${lastImport.reverted} produit${lastImport.reverted > 1 ? "s" : ""} publié${lastImport.reverted > 1 ? "s" : ""} absent${lastImport.reverted > 1 ? "s" : ""} de l'import`, desc: "Repassés « à vérifier » : le fournisseur ne les liste plus.", action: "Vérifier", href: "#/catalog?status=to_review" });
  }

  // Alerts the user has marked as handled drop off the list; the rest still
  // link straight to the page where the problem can be acted on.
  const visibleAlerts = alerts.filter((a) => !dismissed.includes(a.title));
  const dismissAlert = (title) => {
    setDismissed((prev) => {
      const next = prev.includes(title) ? prev : [...prev, title];
      saveDismissedAlerts(next);
      return next;
    });
  };

  const refresh = async () => {
    if (refreshing) return;
    setRefreshing(true);
    try { await window.AxluStore.loadFromApi(); toast("Données à jour", { tone: "success" }); }
    catch (e) { toast("Échec du rafraîchissement", { tone: "danger" }); }
    finally { setRefreshing(false); }
  };


  const kpiHref = {
    imported: "#/catalog",
    toReview: "#/catalog?status=to_review",
    published: "#/shopify?tab=published",
    errors: "#/shopify?tab=errors",
  };

  // Live subtitle based on actual date
  const today = new Date();
  const subtitle = `Aperçu opérationnel · ${today.toLocaleDateString("fr-FR", { weekday: "long", day: "2-digit", month: "long", year: "numeric" })}`;

  // Live counts for shortcut tiles
  const connectedSuppliers = SUPPLIERS.filter((s) => supplierStatus(s) === "connected").length;
  const activeRules = (MARKING_GRIDS || []).filter((g) => g.enabled !== false).length;
  const pendingShopify = (PUBLICATIONS_QUEUE || []).filter((p) => p.status === "queued").length;

  // Pipeline values from real data
  const pipelineRaw = SUPPLIERS.reduce((s, x) => s + supplierStats(x).products, 0);
  const pipelineNorm = PRODUCTS.length;
  const pipelinePub = counts.published;

  return (
    <div>
      <PageHeader
        title={<span className="flex items-center gap-2">Tableau de bord <span aria-hidden>👋</span></span>}
        subtitle={subtitle}
        actions={
          <Button variant="secondary" leftIcon={refreshing ? "sync" : "refresh"} onClick={refresh} disabled={refreshing}>{refreshing ? "Actualisation…" : "Actualiser"}</Button>
        }
      />

      <div className="p-6 space-y-6">
        <div className="grid grid-cols-4 gap-4">
          <div onClick={() => navigate(kpiHref.imported)} className="cursor-pointer rounded-md hover:bg-[var(--hover)] transition-colors">
            <KPI label="📦 Produits importés" value={<CountUp to={counts.imported}/>} icon="catalog"/>
          </div>
          <div onClick={() => navigate(kpiHref.toReview)} className="cursor-pointer rounded-md hover:bg-[var(--hover)] transition-colors">
            <KPI label="👀 À valider"          value={<CountUp to={counts.toReview}/>} icon="warning"/>
          </div>
          <div onClick={() => navigate(kpiHref.published)} className="cursor-pointer rounded-md hover:bg-[var(--hover)] transition-colors">
            <KPI label="🚀 Publiés sur Shopify" value={<CountUp to={counts.published}/>} icon="cart"/>
          </div>
          <div onClick={() => navigate(kpiHref.errors)} className="cursor-pointer rounded-md hover:bg-[var(--hover)] transition-colors">
            <KPI label="💥 En erreur"          value={<CountUp to={counts.errors}/>} icon="cross"/>
          </div>
        </div>

        <div className="grid grid-cols-3 gap-6">
          <div className="col-span-2 space-y-6">
            <Card title="Alertes" padding="none" action={<Badge tone={visibleAlerts.length > 0 ? "warning" : "muted"}>{visibleAlerts.length}</Badge>}>
              {visibleAlerts.length === 0 ? (
                <EmptyState icon="check2" title="Aucune alerte" description="Tout est nominal. Lancez une synchro pour commencer."/>
              ) : (
                <div>
                  {visibleAlerts.map((a) => (
                    <div key={a.title} className="flex items-start gap-2 px-4 py-3 hover-row border-b last:border-b-0 transition-colors" style={{ borderColor: "var(--border)" }}>
                      <a href={a.href} className="flex gap-3 flex-1 min-w-0">
                        <div className={`mt-0.5 shrink-0 ${a.tone === "danger" ? "text-red-600" : a.tone === "warning" ? "text-amber-600" : "text-indigo-600"}`}>
                          <Icon name={a.icon} size={16}/>
                        </div>
                        <div className="flex-1 min-w-0">
                          <div className="text-[13px] font-medium text-app">{a.title}</div>
                          <div className="text-[12px] text-muted mt-0.5">{a.desc}</div>
                        </div>
                        <div className="shrink-0 self-center text-[12px] accent-fg font-medium flex items-center gap-1">
                          {a.action} <Icon name="chevronRight" size={12}/>
                        </div>
                      </a>
                      <button type="button" onClick={() => dismissAlert(a.title)}
                              title="Marquer comme traitée"
                              className="shrink-0 text-subtle hover:text-app p-1 rounded hover:bg-[var(--hover)]">
                        <Icon name="x" size={14}/>
                      </button>
                    </div>
                  ))}
                </div>
              )}
            </Card>

            <Card title="Activité récente" padding="none" action={<a href="#/logs" className="text-[12px] accent-fg hover:underline">Tout voir</a>}>
              {AUDIT_LOG.length === 0 ? (
                <EmptyState icon="log" title="Aucune activité" description="Les actions des utilisateurs apparaîtront ici."/>
              ) : (
                <div>
                  {AUDIT_LOG.slice(0, 8).map((row, i) => (
                    <div key={i} className="flex items-center gap-3 px-4 py-2.5 border-b last:border-b-0 hover-row" style={{ borderColor: "var(--border)" }}>
                      <Avatar name={row.user} size={24}/>
                      <div className="flex-1 min-w-0">
                        <div className="text-[12.5px] truncate">
                          <span className="font-medium">{row.user}</span>{" "}
                          <span className="text-muted">{actionLabel(row.action)}</span>{" "}
                          <span className="text-app">{row.target}</span>
                        </div>
                      </div>
                      <div className="text-[11px] text-subtle whitespace-nowrap mono">{fmt.rel(row.ts)}</div>
                    </div>
                  ))}
                </div>
              )}
            </Card>
          </div>

          <div className="space-y-6">
            <Card title="Synchros récentes" padding="none" action={<a href="#/syncs" className="text-[12px] accent-fg hover:underline">Tout voir</a>}>
              {SYNCS.length === 0 ? (
                <EmptyState icon="sync" title="Aucune synchro" description="Lancez votre première synchronisation."/>
              ) : (
                <div>
                  {SYNCS.slice(0, 6).map((s) => {
                    const sup = SUPPLIERS.find((x) => x.id === s.supplierId);
                    const flowEmoji = (window.AxluData.FLOW_EMOJI || {})[s.type] || "📥";
                    return (
                      <a key={s.id} href={`#/syncs/${s.id}`} className="flex items-center gap-3 px-4 py-2.5 border-b last:border-b-0 hover-row" style={{ borderColor: "var(--border)" }}>
                        <StatusDot status={s.status}/>
                        <div className="flex-1 min-w-0">
                          <div className="text-[12.5px] font-medium truncate flex items-center gap-1.5">
                            <span aria-hidden>{sup?.emoji}</span>
                            <span>{sup?.name || "—"}</span>
                            <span className="text-subtle">·</span>
                            <span aria-hidden>{flowEmoji}</span>
                            <span>{flowLabel(s.type)}</span>
                          </div>
                          <div className="text-[11px] text-muted mono">{fmt.rel(s.startedAt)} · {fmt.duration(s.duration)}</div>
                        </div>
                        <StatusBadge status={s.status} map={SYNC_STATUSES}/>
                      </a>
                    );
                  })}
                </div>
              )}
            </Card>

            <Card title="Raccourcis" padding="sm">
              <div className="grid grid-cols-2 gap-2">
                <ShortcutTile icon="catalog" label="Catalogue" sub={`${PRODUCTS.length} produit${PRODUCTS.length > 1 ? "s" : ""}`} href="#/catalog"/>
                <ShortcutTile icon="warning" label="À valider" sub={`${counts.toReview} produit${counts.toReview > 1 ? "s" : ""}`} href="#/catalog?status=to_review"/>
                <ShortcutTile icon="tag" label="Mapping catégories" sub={unmappedCats > 0 ? `${unmappedCats} manquante${unmappedCats > 1 ? "s" : ""}` : "À jour"} href="#/categories"/>
                <ShortcutTile icon="coin" label="Règles de prix" sub={`${activeRules} active${activeRules > 1 ? "s" : ""}`} href="#/pricing"/>
                <ShortcutTile icon="cart" label="File Shopify" sub={`${pendingShopify} en attente`} href="#/shopify"/>
                <ShortcutTile icon="plug" label="Fournisseurs" sub={`${connectedSuppliers} connecté${connectedSuppliers > 1 ? "s" : ""}`} href="#/suppliers"/>
              </div>
            </Card>

            <Card title="Pipeline" padding="md">
              <div className="text-[12px] text-muted mb-3">Trois niveaux toujours séparés.</div>
              <div className="flex items-stretch gap-2">
                {[
                  { l: "Fournisseur", v: fmt.num(pipelineRaw), s: "Brut", t: "neutral" },
                  { l: "AXLU", v: fmt.num(pipelineNorm), s: "Normalisé", t: "info" },
                  { l: "Shopify", v: fmt.num(pipelinePub), s: "Publié", t: "success" },
                ].map((c, i, arr) => (
                  <Fragment key={i}>
                    <div className="flex-1 sunken rounded-md p-2.5">
                      <Badge tone={c.t}>{c.l}</Badge>
                      <div className="text-[18px] mono font-semibold mt-1.5 tabular-nums">{c.v}</div>
                      <div className="text-[10px] text-subtle uppercase tracking-wide">{c.s}</div>
                    </div>
                    {i < arr.length - 1 && <div className="self-center text-subtle"><Icon name="chevronRight" size={14}/></div>}
                  </Fragment>
                ))}
              </div>
            </Card>
          </div>
        </div>
      </div>
    </div>
  );
}

function actionLabel(a) {
  return ({
    "login": "s'est connecté",
    "product.publish": "a publié",
    "product.approve": "a approuvé",
    "product.update": "a modifié",
    "product.block": "a bloqué",
    "sync.completed": "a complété",
    "sync.started": "a démarré",
    "sync.failed": "a échoué sur",
    "category.map": "a mappé",
    "rule.update": "a modifié la règle",
    "user.create": "a créé l'utilisateur",
  })[a] || a;
}

function flowLabel(type) {
  // Defensive: a sync's `type` may legacy-wrongly be a feed object.
  var key = (type && typeof type === "object") ? (type.type || "") : type;
  return ({
    products: "Produits", products_usb: "Produits WS",
    prices: "Prix produits", prices_usb: "Prix produit USB", prices_ws: "Prix produits WS",
    stocks: "Stocks", marking: "Marquage", images: "Images",
    print_data: "Données d'impression", print_data_ws: "Données d'impression WS",
    print_data_label: "Données d'impression LABEL",
    print_prices: "Prix d'impression", print_prices_usb: "Prix d'impression WS",
    attributes: "Attributs produits",
  })[key] || String(key || "—");
}

function StatusDot({ status }) {
  const colors = {
    success: "bg-emerald-500", partial_success: "bg-amber-500",
    failed: "bg-red-500", running: "bg-indigo-500",
  };
  return (
    <span className={`relative flex w-2 h-2 ${status === "running" ? "" : ""}`}>
      {status === "running" && <span className="absolute inline-flex w-full h-full rounded-full bg-indigo-400 opacity-50 animate-ping"/>}
      <span className={`relative w-2 h-2 rounded-full ${colors[status]}`}/>
    </span>
  );
}

function ShortcutTile({ icon, label, sub, href }) {
  return (
    <a href={href} className="surface border rounded-md p-2.5 hover:bg-[var(--hover)] transition-colors" style={{ borderColor: "var(--border)" }}>
      <div className="flex items-center gap-2">
        <span className="accent-fg"><Icon name={icon} size={14}/></span>
        <span className="text-[12.5px] font-medium">{label}</span>
      </div>
      <div className="text-[11px] text-subtle mt-1">{sub}</div>
    </a>
  );
}

// ─── Suppliers list ────────────────────────────────────────────────────
function SuppliersList({ navigate }) {
  const v = useAxluStore();
  const { SUPPLIERS } = window.AxluData;
  const toast = useToast();
  React.useEffect(() => { window.AxluStore.acquireLock && window.AxluStore.acquireLock('page:suppliers'); return () => window.AxluStore.releaseLock && window.AxluStore.releaseLock('page:suppliers'); }, []);
  const [statusFilter, setStatusFilter] = useState("all");
  const [typeFilter, setTypeFilter] = useState("all");
  const [search, setSearch] = useState("");
  const [addOpen, setAddOpen] = useState(false);
  const [newName, setNewName] = useState("");
  const [newType, setNewType] = useState("XML/JSON");
  const [newCountry, setNewCountry] = useState("LU");

  const COUNTRY_LABELS = { LU: "Luxembourg", NL: "Pays-Bas", DE: "Allemagne", FR: "France" };

  const resetAddForm = () => {
    setNewName("");
    setNewType("XML/JSON");
    setNewCountry("LU");
  };

  const closeAdd = () => { setAddOpen(false); resetAddForm(); };

  const submitAdd = () => {
    const name = newName.trim();
    if (!name) return;
    const newId = window.AxluStore.dispatch("supplier.create", {
      data: { name, type: newType, country: COUNTRY_LABELS[newCountry] || newCountry },
    });
    toast(`Fournisseur « ${name} » créé`, { tone: "success" });
    closeAdd();
    if (newId) navigate(`#/suppliers/${newId}`);
  };

  const filtered = SUPPLIERS.filter((s) =>
    (statusFilter === "all" || supplierStatus(s) === statusFilter) &&
    (typeFilter === "all" || s.type === typeFilter) &&
    (!search || s.name.toLowerCase().includes(search.toLowerCase()))
  );

  return (
    <div>
      <LockBanner resource="page:suppliers"/>
      <PageHeader
        breadcrumb={[{ label: "Fournisseurs" }]}
        title="Fournisseurs"
        subtitle={`${SUPPLIERS.length} source${SUPPLIERS.length > 1 ? "s" : ""} · ${SUPPLIERS.filter((s) => supplierStatus(s) === "connected").length} connectée${SUPPLIERS.filter((s) => supplierStatus(s) === "connected").length > 1 ? "s" : ""}`}
        actions={<Button variant="primary" leftIcon="plus" onClick={() => setAddOpen(true)}>Ajouter un fournisseur</Button>}
      />

      <div className="p-6">
        <div className="flex items-center gap-2 mb-4">
          <Input leftIcon="search" placeholder="Rechercher un fournisseur…" value={search} onChange={(e) => setSearch(e.target.value)} className="w-[260px]"/>
          <Select value={statusFilter} onChange={setStatusFilter} options={[
            { value: "all", label: "Tous statuts" },
            { value: "connected", label: "Connecté" },
            { value: "unconfigured", label: "Non configuré" },
          ]}/>
          <Select value={typeFilter} onChange={setTypeFilter} options={[
            { value: "all", label: "Tous types" },
            { value: "API REST", label: "API REST" },
            { value: "XML/JSON", label: "XML/JSON" },
            { value: "CSV", label: "CSV manuel" },
          ]}/>
          <div className="flex-1"/>
          <span className="text-[12px] text-muted">{filtered.length} résultat{filtered.length > 1 ? "s" : ""}</span>
        </div>

        <Card padding="none">
          <table className="axlu-table">
            <thead>
              <tr>
                <th>Fournisseur</th>
                <th>Type</th>
                <th>Statut connexion</th>
                <th>Dernière synchro</th>
                <th className="text-right">Produits</th>
                <th className="text-right">Erreurs</th>
                <th>Flux</th>
                <th></th>
              </tr>
            </thead>
            <tbody>
              {filtered.map((s) => {
                const st = supplierStats(s);
                return (
                <tr key={s.id} className="hover-row cursor-pointer" onClick={() => navigate(`#/suppliers/${s.id}`)}>
                  <td>
                    <div className="flex items-center gap-2.5">
                      <SupplierAvatar supplier={s} size={28}/>
                      <div>
                        <div className="font-medium text-app">{s.name}</div>
                        <div className="text-[11px] text-subtle">{s.country}</div>
                      </div>
                    </div>
                  </td>
                  <td><span className="mono text-[12px] text-muted">{s.type}</span></td>
                  <td><SupplierStatusBadge supplier={s}/></td>
                  <td className="text-muted mono text-[12px]">{fmt.rel(st.lastSync)}</td>
                  <td className="text-right mono tabular-nums">{fmt.num(st.products)}</td>
                  <td className="text-right">
                    {st.errors > 0 ? (
                      <Badge tone="danger">{st.errors}</Badge>
                    ) : <span className="text-subtle">0</span>}
                  </td>
                  <td>
                    <div className="flex items-center gap-1 flex-wrap" style={{ maxWidth: 220 }}>
                      {window.AxluStore.normalizeFeeds(s).map((f) => {
                        const m = fluxMeta(f.type);
                        const configured = !!f.url || !!m.infoOnly;
                        return (
                          <span key={f.type}
                                className={`inline-flex items-center justify-center w-6 h-6 rounded-md border ${configured ? "accent-bg" : "sunken"}`}
                                style={{ borderColor: "var(--border)", opacity: f.enabled === false ? 0.4 : 1 }}
                                title={m.label + (configured ? "" : " — URL non configurée")}>
                            <Icon name={feedIcon(f.type)} size={13} className={configured ? "accent-fg" : "text-subtle"}/>
                          </span>
                        );
                      })}
                      {window.AxluStore.normalizeFeeds(s).length === 0 && (
                        <span className="text-[11.5px] text-subtle">Aucun flux</span>
                      )}
                    </div>
                  </td>
                  <td>
                    <IconButton icon="chevronRight" size="sm" onClick={(e) => { e.stopPropagation(); navigate(`#/suppliers/${s.id}`); }}/>
                  </td>
                </tr>
                );
              })}
            </tbody>
          </table>
        </Card>
      </div>

      <Modal open={addOpen} onClose={closeAdd} title="Ajouter un fournisseur" width={520}
             footer={<>
               <Button variant="ghost" onClick={closeAdd}>Annuler</Button>
               <Button variant="primary" onClick={submitAdd} disabled={!newName.trim()}>Continuer</Button>
             </>}>
        <div className="space-y-4">
          <div>
            <label className="block text-[12px] font-medium text-muted mb-1.5">Nom du fournisseur</label>
            <Input placeholder="Nom du fournisseur" size="lg" value={newName} onChange={(e) => setNewName(e.target.value)}/>
          </div>
          <div>
            <label className="block text-[12px] font-medium text-muted mb-1.5">Type d'intégration</label>
            <div className="grid grid-cols-3 gap-2">
              {[
                { label: "API REST",   desc: "Connexion temps réel" },
                { label: "XML/JSON",   desc: "Feeds périodiques" },
                { label: "CSV manuel", desc: "Import à la demande" },
              ].map((opt) => (
                <button key={opt.label} type="button" onClick={() => setNewType(opt.label)} className={`surface border rounded-md p-3 text-left ${newType === opt.label ? "ring-2" : ""}`} style={{ borderColor: "var(--border)", "--tw-ring-color": "var(--accent)" }}>
                  <div className="text-[13px] font-medium">{opt.label}</div>
                  <div className="text-[11px] text-subtle mt-0.5">{opt.desc}</div>
                </button>
              ))}
            </div>
          </div>
          <div>
            <label className="block text-[12px] font-medium text-muted mb-1.5">Pays</label>
            <Select value={newCountry} onChange={setNewCountry} options={[
              { value: "LU", label: "Luxembourg" }, { value: "NL", label: "Pays-Bas" }, { value: "DE", label: "Allemagne" }, { value: "FR", label: "France" },
            ]}/>
          </div>
          <div className="sunken rounded-md p-3 text-[12px] text-muted">
            <Icon name="info" size={14} className="inline mr-1.5 align-text-bottom"/>
            Vous pourrez configurer les flux (URL + cadence) et les règles métier après création.
          </div>
        </div>
      </Modal>
    </div>
  );
}

// Status reflects reality: "Connecté" once a feed has a URL, else "Non configuré".
function SupplierStatusBadge({ supplier }) {
  if (supplier && supplier.enabled === false) return <Badge tone="warning" dot>Imports désactivés</Badge>;
  return supplierStatus(supplier) === "connected"
    ? <Badge tone="success" dot>Connecté</Badge>
    : <Badge tone="muted">Non configuré</Badge>;
}

// ─── Supplier detail ──────────────────────────────────────────────────
function SupplierDetail({ supplierId, navigate }) {
  const v = useAxluStore();
  const { SUPPLIERS, SYNCS, SYNC_STATUSES } = window.AxluData;
  const supplier = SUPPLIERS.find((s) => s.id === supplierId);
  const [tab, setTab] = useState("config");
  const [syncProgress, setSyncProgress] = useState(null); // null = idle, else label
  const toast = useToast();
  React.useEffect(() => { window.AxluStore.acquireLock && window.AxluStore.acquireLock('page:suppliers'); return () => window.AxluStore.releaseLock && window.AxluStore.releaseLock('page:suppliers'); }, []);

  // Guard: supplier may be undefined during a delete (re-render fires before navigation).
  React.useEffect(() => {
    if (!supplier) navigate("#/suppliers");
  }, [supplier]);
  if (!supplier) return null;

  const supplierSyncs = SYNCS.filter((s) => s.supplierId === supplier.id);
  const stats = supplierStats(supplier);

  // Launch a REAL sync of every configured flux, in dependency order
  // (Produits → Stocks → Prix → Données d'impression → Prix d'impression → …).
  const launchAllSyncs = async () => {
    if (syncProgress) return;
    if (supplier.enabled === false) { toast("Imports désactivés — réactivez-les dans l'onglet Configuration", { tone: "warning" }); return; }
    setSyncProgress("Démarrage…");
    toast("Synchronisation de tous les flux — ordre Produits → Stocks → Prix → …", { tone: "info" });
    const summary = await runSupplierFeedsInOrder(supplier, {
      toast,
      onProgress: (p) => {
        if (p.phase === "start") setSyncProgress(`Synchro ${p.index + 1}/${p.total}…`);
        else if (p.phase === "error") toast(`${p.label} — échec : ${p.message}`, { tone: "danger" });
        else if (p.phase === "done") toast(`${p.label} — ${p.message}`, { tone: "success" });
      },
    });
    if (summary && summary.jobStarted) {
      setSyncProgress("Import en cours en arrière-plan…");
      setTimeout(() => setSyncProgress(null), 125000);
    } else {
      setSyncProgress(null);
      if (summary.total > 0) {
        toast(`Synchronisation terminée — ${summary.ran}/${summary.total} flux OK${summary.failed ? ` · ${summary.failed} en échec` : ""}`,
              { tone: summary.failed ? "warning" : "success" });
        setTab("feeds");
      }
    }
  };

  return (
    <div>
      <LockBanner resource="page:suppliers"/>
      <PageHeader
        onBack={() => navigate("#/suppliers")}
        breadcrumb={[{ label: "Fournisseurs", href: "#/suppliers" }, { label: supplier.name }]}
        title={
          <span className="flex items-center gap-3">
            <SupplierAvatar supplier={supplier} size={32}/>
            <span>{supplier.name}</span>
            <SupplierStatusBadge supplier={supplier}/>
          </span>
        }
        subtitle={
          <span className="flex items-center gap-3 mono text-[12px]">
            <span>{supplier.type}</span>
            <span className="text-subtle">·</span>
            <span>{supplier.country}</span>
            <span className="text-subtle">·</span>
            <span>Dernière synchro {fmt.rel(stats.lastSync)}</span>
          </span>
        }
        actions={
          <Button variant="primary" leftIcon={syncProgress ? "sync" : "play"} onClick={launchAllSyncs}
                  disabled={supplier.enabled === false || !!syncProgress}
                  title={supplier.enabled === false
                    ? "Imports désactivés pour ce fournisseur"
                    : "Télécharge et importe tous les flux configurés, dans l'ordre"}>
            {syncProgress || "Lancer toutes les synchros"}
          </Button>
        }
        tabs={
          <Tabs value={tab} onChange={setTab} tabs={[
            { id: "config", label: "Configuration" },
            { id: "feeds", label: "Flux", count: (supplier.feeds || []).length },
            { id: "rules", label: "Règles métier" },
            { id: "history", label: "Historique synchros", count: supplierSyncs.length },
          ]}/>
        }
      />

      <div className="p-6">
        {tab === "config" && <SupplierConfigTab supplier={supplier} onDeleted={() => navigate("#/suppliers")}/>}
        {tab === "feeds" && <SupplierFeedsTab supplier={supplier} navigate={navigate}/>}
        {tab === "rules" && <SupplierRulesTab supplier={supplier} navigate={navigate}/>}
        {tab === "history" && <SupplierHistoryTab syncs={supplierSyncs} navigate={navigate}/>}
      </div>
    </div>
  );
}

// Collect distinct auto-sync trigger slots from every supplier's feed
// schedules — manual / non-ready / info-only flux contribute nothing.
function computeSyncTriggers() {
  const out = [];
  const seen = {};
  (window.AxluData.SUPPLIERS || []).forEach((sup) => {
    window.AxluStore.normalizeFeeds(sup).forEach((f) => {
      if (f.enabled === false) return;
      const meta = fluxMeta(f.type);
      if (meta.parserReady === false || meta.infoOnly) return;
      const sc = normalizeSchedule(f.schedule);
      if (sc.mode === "daily") {
        (sc.times || []).forEach((t) => {
          if (!/^\d{1,2}:\d{2}$/.test(t)) return;
          const k = "d|" + t;
          if (!seen[k]) { seen[k] = 1; out.push({ kind: "daily", time: t }); }
        });
      } else if (sc.mode === "weekly") {
        DAYS.forEach((d) => {
          const t = sc.days[d.id];
          if (!t || !/^\d{1,2}:\d{2}$/.test(t)) return;
          const k = "w|" + d.id + "|" + t;
          if (!seen[k]) { seen[k] = 1; out.push({ kind: "weekly", day: d.id, time: t }); }
        });
      }
    });
  });
  return out;
}

function fmtTriggerList(triggers) {
  const DAY_LBL = { mon: "Lun", tue: "Mar", wed: "Mer", thu: "Jeu", fri: "Ven", sat: "Sam", sun: "Dim" };
  return triggers.map((t) => t.kind === "weekly"
    ? `${DAY_LBL[t.day] || t.day} · ${hhmm(t.time)}`
    : `Tous les jours · ${hhmm(t.time)}`);
}

// Card on the supplier Configuration tab: enable/disable the Windows
// scheduled task that runs the import automatically, app closed.
function AutoSyncCard() {
  const toast = useToast();
  const [status, setStatus] = useState(null);
  const [busy, setBusy] = useState(false);
  const triggers = computeSyncTriggers();
  const available = !!(window.axlu && window.axlu.scheduler);

  const refresh = () => {
    if (!available) { setStatus({ unavailable: true }); return; }
    window.axlu.scheduler.status()
      .then((s) => setStatus(s || { registered: false }))
      .catch(() => setStatus({ registered: false }));
  };
  React.useEffect(() => { refresh(); }, []);

  const enable = async () => {
    if (busy || !available) return;
    if (triggers.length === 0) { toast("Aucune cadence planifiée — réglez la cadence des flux d'abord", { tone: "warning" }); return; }
    setBusy(true);
    let res;
    try { res = await window.axlu.scheduler.apply({ triggers }); }
    catch (e) { res = { ok: false, error: String(e && e.message || e) }; }
    setBusy(false);
    toast(res && res.ok
      ? `Synchronisation automatique activée — ${res.count} créneau${res.count > 1 ? "x" : ""}`
      : "Échec de l'activation : " + ((res && res.error) || "inconnu"),
      { tone: res && res.ok ? "success" : "danger" });
    refresh();
  };
  const disable = async () => {
    if (busy || !available) return;
    setBusy(true);
    try { await window.axlu.scheduler.clear(); } catch (e) { /* ignore */ }
    setBusy(false);
    toast("Synchronisation automatique désactivée", { tone: "warning" });
    refresh();
  };

  const registered = !!(status && status.registered);
  const times = fmtTriggerList(triggers);

  return (
    <Card title="Synchronisation automatique">
      <div className="text-[12.5px] text-muted mb-3">
        Windows lance AXLU tout seul aux heures de cadence configurées et importe les flux en arrière-plan,
        <strong> même application fermée</strong>. Si le PC est en veille, Windows le réveille brièvement
        (écran éteint) pour faire la synchro, puis il se rendort. Ne fonctionne pas si le PC est éteint.
      </div>
      {status && status.unavailable ? (
        <div className="sunken rounded-md p-3 text-[12px] text-amber-700 dark:text-amber-300">
          Disponible uniquement dans l'application de bureau Windows.
        </div>
      ) : (
        <>
          <div className="flex items-center justify-between py-2 border-b" style={{ borderColor: "var(--border)" }}>
            <div>
              <div className="text-[13px] font-medium">{registered ? "Activée" : "Désactivée"}</div>
              {registered ? (
                <div className="text-[11.5px] text-subtle leading-relaxed">
                  <div>Prochaine : {status && status.nextRun ? fmt.date(status.nextRun) : "—"}</div>
                  <div>Dernière : {status && status.lastRun ? fmt.date(status.lastRun) : "jamais encore exécutée"}</div>
                </div>
              ) : (
                <div className="text-[11.5px] text-subtle">Aucune tâche Windows enregistrée</div>
              )}
            </div>
            <button onClick={registered ? disable : enable} disabled={busy}
                    title={registered ? "Désactiver" : "Activer"}
                    className="w-9 h-5 rounded-full relative transition-colors shrink-0"
                    style={{ background: registered ? "var(--accent)" : "var(--border-strong)", opacity: busy ? 0.5 : 1 }}>
              <span className="absolute top-0.5 w-4 h-4 bg-white rounded-full transition-all" style={{ left: registered ? 18 : 2 }}/>
            </button>
          </div>
          <div className="mt-3">
            <div className="text-[11px] uppercase tracking-wide text-subtle mb-1.5">Créneaux planifiés ({times.length})</div>
            {times.length === 0 ? (
              <div className="text-[12px] text-muted">Aucun — réglez la cadence des flux dans l'onglet Flux.</div>
            ) : (
              <div className="flex flex-wrap gap-1.5">
                {times.map((t, i) => (
                  <span key={i} className="px-2 py-0.5 rounded sunken text-[11.5px] mono text-muted">{t}</span>
                ))}
              </div>
            )}
          </div>
          <div className="flex flex-wrap gap-2 mt-3">
            {registered && (
              <Button size="sm" variant="secondary" disabled={busy} onClick={enable}>
                Ré-appliquer (après modif. des cadences)
              </Button>
            )}
            <Button size="sm" variant="ghost" onClick={() => window.axlu.scheduler.openScheduler()}>
              Ouvrir le Planificateur Windows
            </Button>
          </div>
        </>
      )}
    </Card>
  );
}

function SupplierConfigTab({ supplier, onDeleted }) {
  const v = useAxluStore();
  const [confirmDelete, setConfirmDelete] = useState(false);
  const logoInputRef = useRef(null);
  const patch = (p) => window.AxluStore.dispatch("supplier.update", { id: supplier.id, patch: p });
  const stats = supplierStats(supplier);

  const handleLogo = (file) => {
    if (!file) return;
    const r = new FileReader();
    r.onload = () => patch({ logo: r.result });
    r.readAsDataURL(file);
  };

  return (
    <div className="grid grid-cols-3 gap-6">
      <div className="col-span-2 space-y-4">
        <Card title="Logo & identité">
          <div className="flex items-center gap-4">
            <SupplierAvatar supplier={supplier} size={56}/>
            <div className="space-y-2">
              <div className="flex gap-2">
                <input ref={logoInputRef} type="file" accept="image/*" style={{ display: "none" }}
                       onChange={(e) => handleLogo(e.target.files && e.target.files[0])}/>
                <Button size="sm" variant="secondary" leftIcon="upload" onClick={() => logoInputRef.current && logoInputRef.current.click()}>
                  {supplier.logo ? "Changer le logo" : "Ajouter le logo du fournisseur"}
                </Button>
                {supplier.logo && <Button size="sm" variant="ghost" onClick={() => patch({ logo: null })}>Retirer</Button>}
              </div>
              <div className="text-[11px] text-subtle">PNG ou JPG, format carré de préférence. Remplace l'icône par défaut partout dans l'application.</div>
            </div>
          </div>
        </Card>

        {(supplier.type === "CSV manuel" || supplier.type === "CSV") ? (
          <Card title="Import CSV manuel">
            <div className="sunken rounded-md p-6 text-center border-2 border-dashed" style={{ borderColor: "var(--border)" }}>
              <Icon name="upload" size={24} className="mx-auto mb-2 text-muted"/>
              <div className="text-[13px] font-medium">Glissez un fichier CSV ici</div>
              <div className="text-[12px] text-muted mt-1">ou cliquez pour parcourir</div>
            </div>
          </Card>
        ) : (
          <Card title="Connexion aux flux">
            <div className="sunken rounded-md p-3 text-[12px] text-muted flex items-start gap-2">
              <Icon name="info" size={14} className="mt-0.5 shrink-0 accent-fg"/>
              <div>Ce fournisseur est connecté via des <strong>URL de flux JSON/XML</strong> — aucun identifiant de compte n'est requis. Les URL de chaque flux et leur cadence se règlent dans l'onglet <strong>Flux</strong>.</div>
            </div>
          </Card>
        )}

        <Card title="Fuseau horaire">
          <Field label="Fuseau utilisé pour la planification des flux">
            <Select value={supplier.timezone || "europe-lu"} onChange={(val) => patch({ timezone: val })} options={[
              { value: "europe-lu", label: "Europe/Luxembourg" },
              { value: "europe-paris", label: "Europe/Paris" },
              { value: "europe-berlin", label: "Europe/Berlin" },
              { value: "utc", label: "UTC" },
            ]}/>
          </Field>
        </Card>

        <AutoSyncCard/>
      </div>

      <div className="space-y-4">
        <Card title="État" padding="md">
          <dl className="space-y-2.5 text-[13px]">
            <Row k="Statut"><SupplierStatusBadge supplier={supplier}/></Row>
            <Row k="Type"><span className="mono">{supplier.type}</span></Row>
            <Row k="Pays">{supplier.country}</Row>
            <Row k="Produits importés"><span className="mono tabular-nums">{fmt.num(stats.products)}</span></Row>
            <Row k="Synchros lancées"><span className="mono tabular-nums">{fmt.num(stats.syncCount)}</span></Row>
            <Row k="Synchros en échec">{stats.errors > 0 ? <Badge tone="danger">{stats.errors}</Badge> : <span className="text-subtle">0</span>}</Row>
            <Row k="Dernière synchro" mono>{fmt.rel(stats.lastSync)}</Row>
          </dl>
        </Card>

        <Card title="Zone dangereuse" padding="md">
          <Button variant="secondary" className="w-full mb-2" onClick={() => {
            window.AxluStore.dispatch("supplier.toggleEnabled", { id: supplier.id });
          }}>{supplier.enabled === false ? "Réactiver les imports" : "Désactiver les imports"}</Button>
          <Button variant="danger" className="w-full" disabled={!window.AxluStore.can("delete")} title={!window.AxluStore.can("delete") ? "Réservé aux administrateurs" : undefined} onClick={() => setConfirmDelete(true)}>Supprimer ce fournisseur</Button>
          <div className="text-[11px] text-subtle mt-2">La suppression conserve les produits déjà importés mais coupe la synchronisation.</div>
        </Card>
      </div>

      <Modal open={confirmDelete} onClose={() => setConfirmDelete(false)} title="Supprimer le fournisseur ?" width={460}
             footer={<>
               <Button variant="ghost" onClick={() => setConfirmDelete(false)}>Annuler</Button>
               <Button variant="danger" onClick={() => {
                 window.AxluStore.dispatch("supplier.delete", { id: supplier.id });
                 setConfirmDelete(false);
                 if (typeof onDeleted === "function") onDeleted();
               }}>Supprimer définitivement</Button>
             </>}>
        <div className="text-[13px]">Supprimer définitivement « <span className="font-medium">{supplier.name}</span> » ? Cette action est irréversible.</div>
      </Modal>
    </div>
  );
}

function Field({ label, children }) {
  return (
    <div>
      <label className="block text-[12px] font-medium text-muted mb-1.5">{label}</label>
      {children}
    </div>
  );
}

function Row({ k, children, mono }) {
  return (
    <div className="flex items-center justify-between">
      <dt className="text-muted">{k}</dt>
      <dd className={mono ? "mono text-[12px]" : ""}>{children}</dd>
    </div>
  );
}

// Catalogue of all flux types the app supports (PF Concept-aware presets).
// `parserReady` reflects whether the backend parser is implemented yet —
// non-ready flux can still have their URL/cadence saved but Importer is disabled.
const ALL_FLUX = [
  { id: "products",         label: "Produits",                       presetSchedule: "daily-05",       parserReady: true },
  { id: "products_usb",     label: "Produits WS",                    presetSchedule: "daily-05",       parserReady: true,
    defaultUrl: "https://www.pfconcept.com/portal/datafeed/productfeedws_fr_v3.json" },
  { id: "stocks",           label: "Stocks",                         presetSchedule: "twice-02-13",    parserReady: true },
  { id: "prices",           label: "Prix produits",                  presetSchedule: "weekly-sat-14",  parserReady: true },
  { id: "prices_usb",       label: "Prix produit USB",               presetSchedule: "daily-02",       parserReady: true },
  { id: "prices_ws",        label: "Prix produits WS",               presetSchedule: "weekly-sat-14",  parserReady: true,
    defaultUrl: "https://www.pfconcept.com/portal/datafeed/pricefeedWS_CFR1_v3.json" },
  { id: "print_data",       label: "Données d'impression",           presetSchedule: "daily-06",       parserReady: true },
  { id: "print_data_ws",    label: "Données d'impression WS",        presetSchedule: "daily-06",       parserReady: true,
    defaultUrl: "https://www.pfconcept.com/portal/datafeed/printdataws_cfr1_fr_v3.json" },
  { id: "print_data_label", label: "Données d'impression LABEL",     presetSchedule: "daily-06",       parserReady: true,
    defaultUrl: "https://www.pfconcept.com/portal/datafeed/printdata_cfr1_fr_label_v3.json" },
  { id: "print_prices",     label: "Prix d'impression",              presetSchedule: "weekly-sat-14",  parserReady: true },
  { id: "print_prices_usb", label: "Prix d'impression WS",           presetSchedule: "weekly-sat-14",  parserReady: true },
  { id: "attributes",       label: "Attributs produits",             presetSchedule: "weekly-sat-14",  parserReady: true },
  { id: "images",           label: "Images",                         presetSchedule: "manual",         parserReady: false, infoOnly: true },
];

function fluxMeta(id) {
  return ALL_FLUX.find((f) => f.id === id) || { id, label: flowLabel(id), presetSchedule: "manual" };
}

// ─── Feed schedule — manual / daily (N times) / weekly (days + times) ──
const DAYS = [
  { id: "mon", label: "Lun" }, { id: "tue", label: "Mar" }, { id: "wed", label: "Mer" },
  { id: "thu", label: "Jeu" }, { id: "fri", label: "Ven" }, { id: "sat", label: "Sam" }, { id: "sun", label: "Dim" },
];

// Convert a schedule (legacy preset string OR structured object) into the
// structured object { mode, times[], days{} }.
function normalizeSchedule(s) {
  if (s && typeof s === "object") {
    return {
      mode: s.mode || "manual",
      times: Array.isArray(s.times) && s.times.length ? s.times : ["08:00"],
      days: (s.days && typeof s.days === "object") ? s.days : {},
    };
  }
  const str = String(s || "manual");
  if (str === "twice-02-13") return { mode: "daily", times: ["02:00", "13:00"], days: {} };
  let m = /^daily-(\d{2})$/.exec(str);
  if (m) return { mode: "daily", times: [m[1] + ":00"], days: {} };
  m = /^weekly-(\w{3})-(\d{2})$/.exec(str);
  if (m) return { mode: "weekly", times: ["08:00"], days: { [m[1]]: m[2] + ":00" } };
  return { mode: "manual", times: ["08:00"], days: {} };
}

const hhmm = (t) => String(t || "").replace(":", "h");

function scheduleLabel(s) {
  const sc = normalizeSchedule(s);
  if (sc.mode === "daily") return "Quotidien · " + (sc.times || []).map(hhmm).join(", ");
  if (sc.mode === "weekly") {
    const parts = DAYS.filter((d) => sc.days[d.id]).map((d) => d.label + " " + hhmm(sc.days[d.id]));
    return parts.length ? "Hebdo · " + parts.join(", ") : "Hebdomadaire (aucun jour)";
  }
  return "Manuelle";
}

// ─── Auto-sync « due » logic ──────────────────────────────────────────
// A scheduled run fires at the union of all feed trigger times. So a fire
// can't tell which feed it is for — isFeedDue() decides: a feed is due
// when one of its schedule slots has elapsed that it has not covered yet
// (lastRun older than that slot). A feed already run at its slot is
// skipped; a feed that missed its slot (PC asleep) is caught up next run.
function latestPassedSlot(sc, now) {
  let best = null;
  const consider = (d) => {
    if (d.getTime() <= now.getTime() && (!best || d.getTime() > best.getTime())) best = d;
  };
  if (sc.mode === "daily") {
    (sc.times || []).forEach((t) => {
      const m = /^(\d{1,2}):(\d{2})$/.exec(t || "");
      if (!m) return;
      const today = new Date(now); today.setHours(+m[1], +m[2], 0, 0);
      const yest = new Date(today); yest.setDate(yest.getDate() - 1);
      consider(today); consider(yest);
    });
  } else if (sc.mode === "weekly") {
    const dayNum = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
    Object.keys(sc.days || {}).forEach((dk) => {
      const m = /^(\d{1,2}):(\d{2})$/.exec(sc.days[dk] || "");
      if (!m || !(dk in dayNum)) return;
      const d = new Date(now); d.setHours(+m[1], +m[2], 0, 0);
      d.setDate(d.getDate() - ((d.getDay() - dayNum[dk] + 7) % 7));
      if (d.getTime() > now.getTime()) d.setDate(d.getDate() - 7);
      consider(d);
    });
  }
  return best;
}

function isFeedDue(feed, now) {
  const sc = normalizeSchedule(feed && feed.schedule);
  if (sc.mode !== "daily" && sc.mode !== "weekly") return false; // manual → never auto
  const slot = latestPassedSlot(sc, now);
  if (!slot) return false;
  const lastRun = (feed && feed.lastRun) ? new Date(feed.lastRun) : null;
  return !lastRun || lastRun.getTime() < slot.getTime();
}

// Interactive schedule editor used in the feed form.
function ScheduleEditor({ value, onChange }) {
  const sc = normalizeSchedule(value);
  const setTimesCount = (n) => {
    const times = sc.times.slice(0, n);
    while (times.length < n) times.push("08:00");
    onChange({ ...sc, times });
  };
  const setTime = (i, t) => {
    const times = sc.times.slice();
    times[i] = t;
    onChange({ ...sc, times });
  };
  const toggleDay = (day) => {
    const days = { ...sc.days };
    if (days[day]) delete days[day]; else days[day] = "08:00";
    onChange({ ...sc, days });
  };
  const timeInput = { height: 32, borderColor: "var(--border)" };
  return (
    <div className="space-y-2.5">
      <Select value={sc.mode} onChange={(mode) => onChange({ ...sc, mode })} options={[
        { value: "manual", label: "Manuelle uniquement" },
        { value: "daily",  label: "Tous les jours" },
        { value: "weekly", label: "Toutes les semaines" },
      ]}/>
      {sc.mode === "daily" && (
        <div className="sunken rounded-md p-3 space-y-2.5">
          <div className="flex items-center gap-2">
            <span className="text-[12px] text-muted whitespace-nowrap">Nombre de fois par jour :</span>
            <Select value={String(sc.times.length)} onChange={(n) => setTimesCount(parseInt(n, 10) || 1)}
                    options={[1, 2, 3, 4, 5, 6].map((n) => ({ value: String(n), label: String(n) }))}
                    className="max-w-[90px]"/>
          </div>
          <div className="flex flex-wrap gap-2.5">
            {sc.times.map((t, i) => (
              <label key={i} className="flex items-center gap-1.5 text-[12px]">
                <span className="text-subtle">Heure {i + 1}</span>
                <input type="time" value={t} onChange={(e) => setTime(i, e.target.value)}
                       className="surface border rounded-md px-2 text-[13px] ring-accent" style={timeInput}/>
              </label>
            ))}
          </div>
        </div>
      )}
      {sc.mode === "weekly" && (
        <div className="sunken rounded-md p-3 space-y-1.5">
          <div className="text-[12px] text-muted mb-1">Cochez les jours et réglez l'heure de chacun :</div>
          {DAYS.map((d) => {
            const on = !!sc.days[d.id];
            return (
              <div key={d.id} className="flex items-center gap-2.5">
                <button type="button" onClick={() => toggleDay(d.id)}
                        className="w-8 h-5 rounded-full relative transition-colors shrink-0"
                        style={{ background: on ? "var(--accent)" : "var(--border-strong)" }}>
                  <span className="absolute top-0.5 w-4 h-4 bg-white rounded-full transition-all" style={{ left: on ? 14 : 2 }}/>
                </button>
                <span className="text-[12.5px]" style={{ width: 34 }}>{d.label}</span>
                {on ? (
                  <input type="time" value={sc.days[d.id]}
                         onChange={(e) => onChange({ ...sc, days: { ...sc.days, [d.id]: e.target.value } })}
                         className="surface border rounded-md px-2 text-[13px] ring-accent" style={timeInput}/>
                ) : (
                  <span className="text-[11.5px] text-subtle">inactif</span>
                )}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// Live status / stats derived from real data (not seed counters).
function supplierStatus(supplier) {
  const feeds = (supplier && supplier.feeds) || [];
  return feeds.some((f) => f && f.url) ? "connected" : "unconfigured";
}
function supplierStats(supplier) {
  const PRODUCTS = window.AxluData.PRODUCTS || [];
  const SYNCS = window.AxluData.SYNCS || [];
  const products = PRODUCTS.filter((p) => p.supplierName === supplier.name || p.supplier === supplier.id).length;
  const syncs = SYNCS.filter((s) => s.supplierId === supplier.id);
  const lastSync = syncs.reduce((m, s) => (!m || new Date(s.startedAt) > new Date(m)) ? s.startedAt : m, null);
  const errors = syncs.filter((s) => s.status === "failed").length;
  return { products, syncCount: syncs.length, lastSync, errors };
}

// Apply a parsed feed to the store. Pure (no component state) — shared by
// the feed preview import and the "Lancer toutes les synchros" action.
// Returns { ok, message, rowsRead, created, updated, errors }.
function dispatchByType(type, items) {
  const rowsRead = Array.isArray(items) ? items.length : 0;
  const base = { ok: true, rowsRead, created: 0, updated: 0, errors: 0 };
  if (type === "products" || type === "products_usb") {
    const res = window.AxluStore.dispatch("product.import", { items, feedType: type }) || {};
    const parts = [];
    if (res.added) parts.push(`${res.added} ajouté${res.added > 1 ? "s" : ""}`);
    if (res.updated) parts.push(`${res.updated} mis à jour`);
    if (res.flagged) parts.push(`${res.flagged} à vérifier`);
    if (res.reverted) parts.push(`${res.reverted} absent${res.reverted > 1 ? "s" : ""} → à vérifier`);
    if (res.skipped) parts.push(`${res.skipped} ignoré${res.skipped > 1 ? "s" : ""}`);
    return { ...base, created: res.added || 0, updated: res.updated || 0, flagged: res.flagged || 0, message: parts.join(" · ") || "0 produit traité" };
  }
  if (type === "stocks") {
    const res = window.AxluStore.dispatch("product.enrichStock", { items }) || {};
    const parts = [];
    if (res.matched) parts.push(`${res.matched} stock${res.matched > 1 ? "s" : ""} mis à jour`);
    if (res.flagged) parts.push(`${res.flagged} à vérifier`);
    if (res.missing) parts.push(`${res.missing} SKU sans produit existant`);
    return { ...base, updated: res.matched || 0, missing: res.missing || 0, flagged: res.flagged || 0, message: parts.join(" · ") || "0 stock traité" };
  }
  if (type === "prices" || type === "prices_usb" || type === "prices_ws") {
    const res = window.AxluStore.dispatch("product.enrichPrices", { items }) || {};
    const parts = [];
    if (res.matched) parts.push(`${res.matched} prix mis à jour`);
    if (res.flagged) parts.push(`${res.flagged} à vérifier`);
    if (res.missing) parts.push(`${res.missing} SKU sans produit existant`);
    return { ...base, updated: res.matched || 0, missing: res.missing || 0, flagged: res.flagged || 0, message: parts.join(" · ") || "0 prix traité" };
  }
  if (type === "print_data" || type === "print_data_ws" || type === "print_data_label") {
    const res = window.AxluStore.dispatch("product.enrichPrintData", { items }) || {};
    const parts = [];
    if (res.matched) parts.push(`${res.matched} variantes enrichies (méthodes d'impression)`);
    if (res.missing) parts.push(`${res.missing} SKU sans produit existant`);
    return { ...base, updated: res.matched || 0, missing: res.missing || 0, message: parts.join(" · ") || "0 donnée d'impression traitée" };
  }
  if (type === "print_prices" || type === "print_prices_usb") {
    const res = window.AxluStore.dispatch("pf.setPrintPrices", { items }) || {};
    const parts = [];
    if (res.added) parts.push(`${res.added} ajoutés`);
    if (res.updated) parts.push(`${res.updated} mis à jour`);
    return { ...base, created: res.added || 0, updated: res.updated || 0, message: `${parts.join(" · ") || "0 traité"} · barème total : ${res.count || 0} codes` };
  }
  if (type === "attributes") {
    const res = window.AxluStore.dispatch("pf.setAttributes", { items }) || {};
    const parts = [];
    if (res.added) parts.push(`${res.added} ajouté${res.added > 1 ? "s" : ""}`);
    if (res.updated) parts.push(`${res.updated} mis à jour`);
    return { ...base, created: res.added || 0, updated: res.updated || 0,
             message: `${parts.join(" · ") || "0 traité"} · dictionnaire : ${res.count || 0} attributs` };
  }
  return { ...base, ok: false, message: `Type "${type}" pas encore géré côté import` };
}

// Run every configured + enabled flux of a supplier, IN ORDER, for real:
// fetch + parse + apply to the store, then record the run in the history.
// Order follows ALL_FLUX: Produits → Stocks → Prix → Prix USB → Données
// d'impression → Prix d'impression → Attributs. `onProgress({phase,index,
// total,label,message})` fires per feed (phase = start | done | error).
async function runSupplierFeedsInOrder(supplier, opts) {
  opts = opts || {};
  const onProgress = typeof opts.onProgress === "function" ? opts.onProgress : function () {};
  const toast = opts.toast;
  // WEB : l'import tourne côté serveur (les données vivent en base). On appelle
  // l'endpoint de synchro, on rejoue les toasts par flux, puis on recharge le catalogue.
  if (window.__AXLU_WEB__) {
    const types = Array.isArray(opts.onlyTypes) ? opts.onlyTypes : null;
    try {
      const resp = await fetch("/api/suppliers/" + encodeURIComponent(supplier.id) + "/sync", {
        method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" },
        body: JSON.stringify(types ? { types } : {}),
      });
      const r = await resp.json().catch(() => null);
      if (!resp.ok || !r) throw new Error((r && r.error) || ("HTTP " + resp.status));
      // Le serveur a déclenché le JOB Scaleway (import en arrière-plan, sans limite de temps).
      if (r.jobStarted) {
        if (toast) toast("Import lancé en arrière-plan (~1-2 min). Le catalogue se rafraîchira tout seul.", { tone: "success" });
        // Indicateur global "import en cours" (bandeau), relâché + rechargement au bout de ~2 min.
        try { window.AxluData.__importRunning = true; if (window.AxluStore && window.AxluStore.notify) window.AxluStore.notify(); } catch (e) { /* ignore */ }
        setTimeout(function () {
          try {
            window.AxluData.__importRunning = false;
            if (window.AxluStore) { if (window.AxluStore.loadFromApi) window.AxluStore.loadFromApi(); if (window.AxluStore.notify) window.AxluStore.notify(); }
          } catch (e) { /* ignore */ }
        }, 120000);
        return { ok: true, ran: 0, failed: 0, total: 0, jobStarted: true };
      }
      // Repli (import direct conteneur) : on rejoue les toasts par flux + recharge.
      const results = Array.isArray(r.results) ? r.results : [];
      results.forEach((fr, i) => {
        const label = fluxMeta(fr.type).label;
        if (fr.ok) onProgress({ phase: "done", index: i, total: results.length, label, message: (fr.lus != null ? fr.lus + " lignes lues" : "OK") });
        else onProgress({ phase: "error", index: i, total: results.length, label, message: fr.error || "échec" });
      });
      if (window.AxluStore && typeof window.AxluStore.loadFromApi === "function") await window.AxluStore.loadFromApi();
      return { ok: !!r.ok, ran: r.ran || 0, failed: r.failed || 0, total: r.total || 0 };
    } catch (e) {
      if (toast) toast("Échec de la synchro : " + (e && e.message ? e.message : e), { tone: "danger" });
      return { ok: false, ran: 0, failed: 0, total: 0 };
    }
  }
  if (!window.axlu || !window.axlu.feeds) {
    if (toast) toast("API IPC indisponible (relancez l'app)", { tone: "danger" });
    return { ok: false, ran: 0, failed: 0, total: 0 };
  }
  const orderIndex = (t) => {
    const i = ALL_FLUX.findIndex((f) => f.id === t);
    return i < 0 ? 999 : i;
  };
  // dueOnly (scheduled / headless run): keep only feeds whose schedule slot
  // has elapsed and not yet been covered — see isFeedDue(). A manual run
  // leaves dueOnly off and runs every configured feed.
  //
  // BUT: whenever a PRODUCT feed is due (products / products_usb), also run
  // the per-product ENRICHMENT feeds (prices, stocks, print data) in the same
  // pass — even if their own slot isn't due. Otherwise a freshly (re)imported
  // product stays priceless/stockless until the price feed's next slot, and a
  // later product re-import keeps re-emptying it (the « 54 sans prix » loop).
  const ENRICH_FEEDS = ['stocks', 'prices', 'prices_usb', 'prices_ws', 'print_data', 'print_data_ws', 'print_data_label'];
  const now = new Date();
  const ready = window.AxluStore.normalizeFeeds(supplier)
    .filter((f) => f.url && f.enabled !== false && fluxMeta(f.type).parserReady !== false);
  let feeds = ready;
  if (opts.dueOnly) {
    const dueSet = new Set(ready.filter((f) => isFeedDue(f, now)));
    const productImportDue = [...dueSet].some((f) => f.type === 'products' || f.type === 'products_usb');
    feeds = ready.filter((f) => dueSet.has(f) || (productImportDue && ENRICH_FEEDS.indexOf(f.type) !== -1));
  } else if (Array.isArray(opts.onlyTypes) && opts.onlyTypes.length) {
    // Run a specific subset of feeds (e.g. one feed picked in « Lancer une
    // synchro », or a single feed being retried) — for real, like the rest.
    feeds = ready.filter((f) => opts.onlyTypes.indexOf(f.type) !== -1);
  }
  feeds = feeds.sort((a, b) => orderIndex(a.type) - orderIndex(b.type));
  if (feeds.length === 0) {
    if (!opts.dueOnly && toast) toast("Aucun flux prêt : renseignez les URL et activez les flux dans l'onglet Flux", { tone: "warning" });
    return { ok: false, ran: 0, failed: 0, total: 0 };
  }
  let ran = 0, failed = 0;
  for (let i = 0; i < feeds.length; i++) {
    const feed = feeds[i];
    const label = fluxMeta(feed.type).label;
    const startedAt = new Date().toISOString();
    const t0 = Date.now();
    onProgress({ phase: "start", index: i, total: feeds.length, label, type: feed.type });
    try {
      const res = await window.axlu.feeds.fetchAndParse({ url: feed.url, type: feed.type, limit: null });
      if (!res || !res.ok) {
        const errMsg = (res && res.error) || "Erreur inconnue";
        const duration = Math.round((Date.now() - t0) / 1000);
        window.AxluStore.dispatch("supplier.updateFeed", { id: supplier.id, type: feed.type,
          patch: { lastRun: startedAt, lastStatus: "failed", lastError: errMsg } });
        window.AxluStore.dispatch("sync.record", { supplierId: supplier.id, type: feed.type,
          status: "failed", duration, startedAt, errorMsg: errMsg });
        failed++;
        onProgress({ phase: "error", index: i, total: feeds.length, label, message: errMsg });
        continue;
      }
      const applied = dispatchByType(feed.type, res.items);
      const duration = Math.round((Date.now() - t0) / 1000);
      window.AxluStore.dispatch("supplier.updateFeed", { id: supplier.id, type: feed.type,
        patch: { lastRun: startedAt, lastStatus: applied.ok ? "success" : "failed",
                 lastError: applied.ok ? null : applied.message, lastRowsRead: applied.rowsRead } });
      window.AxluStore.dispatch("sync.record", { supplierId: supplier.id, type: feed.type,
        status: applied.ok ? "success" : "failed", duration, startedAt,
        rowsRead: applied.rowsRead, rowsCreated: applied.created, rowsUpdated: applied.updated,
        errors: applied.errors, errorMsg: applied.ok ? null : applied.message,
        message: applied.message, missing: applied.missing, flagged: applied.flagged });
      if (applied.ok) { ran++; onProgress({ phase: "done", index: i, total: feeds.length, label, message: applied.message }); }
      else { failed++; onProgress({ phase: "error", index: i, total: feeds.length, label, message: applied.message }); }
    } catch (e) {
      const duration = Math.round((Date.now() - t0) / 1000);
      const errMsg = (e && e.message) || String(e);
      window.AxluStore.dispatch("supplier.updateFeed", { id: supplier.id, type: feed.type,
        patch: { lastRun: startedAt, lastStatus: "failed", lastError: errMsg } });
      window.AxluStore.dispatch("sync.record", { supplierId: supplier.id, type: feed.type,
        status: "failed", duration, startedAt, errorMsg: errMsg });
      failed++;
      onProgress({ phase: "error", index: i, total: feeds.length, label, message: errMsg });
    }
  }
  // After a real import, keep Shopify in sync automatically (fire-and-forget):
  //  • categories → smart collections + parent links (if the tree changed);
  //  • stock → refresh inventory of published products (if a stock-affecting
  //    feed ran). Both no-op gracefully when no store is connected.
  if (ran > 0 && window.AxluStore) {
    if (typeof window.AxluStore.shopifyAutoSyncCategories === "function") {
      Promise.resolve(window.AxluStore.shopifyAutoSyncCategories()).catch(function () {});
    }
    const inventoryAffected = feeds.some((f) => f.type === "stocks" || f.type === "products" || f.type === "products_usb");
    if (inventoryAffected && typeof window.AxluStore.shopifyAutoPushStock === "function") {
      Promise.resolve(window.AxluStore.shopifyAutoPushStock()).catch(function () {});
    }
  }
  return { ok: failed === 0, ran, failed, total: feeds.length };
}

// Reload ONE product from its supplier's feeds: re-fetch each product /
// per-variant enrichment feed, keep only the items for this product, and
// apply them. Reference feeds (barème de prix d'impression, attributs) are
// skipped — they are not per-product. Used by the product "Recharger" button.
async function reloadProduct(product, opts) {
  opts = opts || {};
  const onProgress = typeof opts.onProgress === "function" ? opts.onProgress : function () {};
  const toast = opts.toast;
  if (!product) return { ok: false, message: "Produit introuvable" };
  if (!window.axlu || !window.axlu.feeds) {
    if (toast) toast("API IPC indisponible (relancez l'app)", { tone: "danger" });
    return { ok: false, message: "API indisponible" };
  }
  const suppliers = window.AxluData.SUPPLIERS || [];
  const supName = String(product.supplierName || "").trim().toLowerCase();
  const supplier = suppliers.find((s) => s.id === product.supplier)
    || suppliers.find((s) => String(s.name || "").trim().toLowerCase() === supName)
    || (suppliers.length === 1 ? suppliers[0] : null);
  if (!supplier) return { ok: false, message: "Fournisseur introuvable pour ce produit" };

  // Feed types that carry per-product / per-variant data.
  const PER_PRODUCT = ["products", "products_usb", "stocks", "prices", "prices_usb", "prices_ws",
    "print_data", "print_data_ws", "print_data_label"];
  const orderIndex = (t) => { const i = ALL_FLUX.findIndex((f) => f.id === t); return i < 0 ? 999 : i; };
  const feeds = window.AxluStore.normalizeFeeds(supplier)
    .filter((f) => f.url && f.enabled !== false && fluxMeta(f.type).parserReady !== false
      && PER_PRODUCT.indexOf(f.type) !== -1)
    .sort((a, b) => orderIndex(a.type) - orderIndex(b.type));
  if (feeds.length === 0) return { ok: false, message: "Aucun flux configuré pour ce fournisseur" };

  const modelCode = product.modelCode || product.sku;
  const variantSkus = {};
  (product.variants || []).forEach((vrt) => { if (vrt && vrt.sku) variantSkus[vrt.sku] = true; });
  let touched = 0, failed = 0;
  for (let i = 0; i < feeds.length; i++) {
    const feed = feeds[i];
    onProgress({ index: i, total: feeds.length, label: fluxMeta(feed.type).label });
    try {
      const res = await window.axlu.feeds.fetchAndParse({ url: feed.url, type: feed.type, limit: null });
      if (!res || !res.ok) { failed++; continue; }
      const isProductFeed = feed.type === "products" || feed.type === "products_usb";
      const items = (res.items || []).filter((it) => isProductFeed
        ? ((it.modelCode || it.sku) === modelCode)
        : !!variantSkus[it.sku]);
      if (items.length === 0) continue;
      const applied = dispatchByType(feed.type, items);
      if (applied && applied.ok) touched++; else failed++;
    } catch (e) { failed++; }
  }
  const parts = [];
  if (touched) parts.push(touched + " flux appliqué" + (touched > 1 ? "s" : ""));
  if (failed) parts.push(failed + " en échec");
  return {
    ok: failed === 0 && touched > 0,
    touched, failed,
    message: parts.length ? "Produit rechargé — " + parts.join(" · ")
                          : "Aucune donnée trouvée pour ce produit dans les flux",
  };
}

function SupplierFeedsTab({ supplier, navigate }) {
  const v = useAxluStore();
  const { SYNCS } = window.AxluData;
  const toast = useToast();
  const [addOpen, setAddOpen] = useState(false);
  const [editFeed, setEditFeed] = useState(null);
  const [removeFeed, setRemoveFeed] = useState(null);
  const [runningType, setRunningType] = useState(null); // feed type currently running
  const [preview, setPreview] = useState(null); // { feed, items, totalAvailable, meta }

  // ─── Run a feed: fetch + parse + open preview modal ─────────────
  const runFeed = async (feed) => {
    if (supplier.enabled === false) { toast("Imports désactivés pour ce fournisseur — réactivez-les dans Configuration", { tone: "warning" }); return; }
    if (!feed.url) { toast("URL non configurée pour ce flux", { tone: "danger" }); return; }
    if (window.__AXLU_WEB__) {
      // Web : pas d'aperçu IPC — on lance le Job pour CE flux uniquement (même chemin que « Lancer une synchro »).
      setRunningType(feed.type);
      try { await runSupplierFeedsInOrder(supplier, { onlyTypes: [feed.type], toast }); }
      finally { setTimeout(() => setRunningType(null), 125000); }
      return;
    }
    if (!window.axlu || !window.axlu.feeds) {
      toast("API IPC indisponible (relancez l'app)", { tone: "danger" });
      return;
    }
    setRunningType(feed.type);
    const previewLimit = 10;
    try {
      const res = await window.axlu.feeds.fetchAndParse({ url: feed.url, type: feed.type, limit: previewLimit });
      if (!res || !res.ok) {
        const errMsg = (res && res.error) || "Erreur inconnue";
        // Persist last status on the feed for visibility in the table.
        window.AxluStore.dispatch("supplier.updateFeed", {
          id: supplier.id, type: feed.type,
          patch: { lastRun: new Date().toISOString(), lastStatus: "failed", lastError: errMsg },
        });
        toast(`Échec : ${errMsg}`, { tone: "danger" });
        return;
      }
      setPreview({ feed, items: res.items, totalAvailable: res.totalAvailable, meta: res.meta, fetchMs: res.fetchMs, bytes: res.bytes });
    } catch (e) {
      toast(`Erreur : ${e.message || e}`, { tone: "danger" });
    } finally {
      setRunningType(null);
    }
  };

  const importPreviewed = () => {
    if (!preview) return;
    const items = preview.items;
    const result = dispatchByType(preview.feed.type, items);
    window.AxluStore.dispatch("supplier.updateFeed", {
      id: supplier.id, type: preview.feed.type,
      patch: { lastRun: new Date().toISOString(), lastStatus: result.ok ? "success" : "failed", lastError: result.ok ? null : result.message, lastRowsRead: items.length },
    });
    toast(result.message, { tone: result.ok ? "success" : "danger" });
    setPreview(null);
  };

  const importAll = async () => {
    if (!preview) return;
    const feed = preview.feed;
    setRunningType(feed.type);
    setPreview(null);
    try {
      const res = await window.axlu.feeds.fetchAndParse({ url: feed.url, type: feed.type, limit: null });
      if (!res || !res.ok) {
        const errMsg = (res && res.error) || "Erreur inconnue";
        window.AxluStore.dispatch("supplier.updateFeed", {
          id: supplier.id, type: feed.type,
          patch: { lastRun: new Date().toISOString(), lastStatus: "failed", lastError: errMsg },
        });
        toast(`Échec import complet : ${errMsg}`, { tone: "danger" });
        return;
      }
      const result = dispatchByType(feed.type, res.items);
      window.AxluStore.dispatch("supplier.updateFeed", {
        id: supplier.id, type: feed.type,
        patch: { lastRun: new Date().toISOString(), lastStatus: result.ok ? "success" : "failed", lastError: result.ok ? null : result.message, lastRowsRead: res.items.length },
      });
      const transferStats = res.fromCache
        ? ` (${(res.bytes / 1024 / 1024).toFixed(1)} Mo · depuis le cache, pas de re-téléchargement)`
        : ` (${(res.bytes / 1024 / 1024).toFixed(1)} Mo téléchargés en ${(res.fetchMs / 1000).toFixed(1)}s)`;
      toast(result.message + transferStats, { tone: result.ok ? "success" : "danger" });
    } catch (e) {
      toast(`Erreur : ${e.message || e}`, { tone: "danger" });
    } finally {
      setRunningType(null);
    }
  };

  const feeds = window.AxluStore.normalizeFeeds(supplier).map((f) => {
    const meta = fluxMeta(f.type);
    const matching = SYNCS.filter((s) => s.supplierId === supplier.id && s.type === f.type);
    const last = matching.sort((a, b) => new Date(b.startedAt) - new Date(a.startedAt))[0];
    return { ...f, label: meta.label, last };
  }).sort((a, b) => {
    // Display in import order — the same order as "Lancer toutes les synchros".
    const ia = ALL_FLUX.findIndex((x) => x.id === a.type);
    const ib = ALL_FLUX.findIndex((x) => x.id === b.type);
    return (ia < 0 ? 999 : ia) - (ib < 0 ? 999 : ib);
  });

  const available = ALL_FLUX.filter((opt) => !feeds.find((f) => f.type === opt.id));

  if (feeds.length === 0) {
    return (
      <>
        <Card padding="lg">
          <EmptyState
            icon="log"
            title="Aucun flux configuré"
            description="Configurez chaque flux que ce fournisseur expose, avec son URL et sa cadence. Chaque flux est indépendant."
            action={
              <Button variant="primary" leftIcon="plus" onClick={() => setAddOpen(true)} disabled={available.length === 0}>
                Ajouter un flux
              </Button>
            }
          />
        </Card>
        <FeedFormModal open={addOpen} onClose={() => setAddOpen(false)} available={available}
                       supplierId={supplier.id} toast={toast} mode="create"/>
      </>
    );
  }

  return (
    <div>
      <div className="mb-3 flex items-center justify-end">
        <Button size="sm" variant="primary" leftIcon="plus" onClick={() => setAddOpen(true)} disabled={available.length === 0}>
          Ajouter un flux
        </Button>
      </div>
      <Card padding="none">
        <table className="axlu-table">
          <thead>
            <tr>
              <th style={{ width: 28 }}></th>
              <th>Flux</th>
              <th>URL</th>
              <th>Cadence</th>
              <th>Statut</th>
              <th>Dernière exécution</th>
              <th className="text-right">Lignes</th>
              <th className="text-right" style={{ width: 200 }}></th>
            </tr>
          </thead>
          <tbody>
            {feeds.map((f) => {
              // A flux whose parser is not implemented yet can't have run for
              // real — ignore stale status / row counts and show a clean
              // state. `infoOnly` flux (images) need no import at all.
              const ready = fluxMeta(f.type).parserReady !== false;
              const infoOnly = !!fluxMeta(f.type).infoOnly;
              const status = ready ? (f.lastStatus || (f.last ? f.last.status : null)) : null;
              const lastTs = ready ? (f.lastRun || (f.last ? f.last.startedAt : null)) : null;
              const lastRows = ready ? (f.lastRowsRead != null ? f.lastRowsRead : (f.last ? f.last.rowsRead : null)) : null;
              return (
                <tr key={f.type} className="hover-row" style={{ opacity: f.enabled === false ? 0.55 : 1 }}>
                  <td>
                    <button onClick={() => window.AxluStore.dispatch("supplier.toggleFeedEnabled", { id: supplier.id, type: f.type })}
                            className="w-7 h-4 rounded-full relative transition-colors"
                            title={f.enabled !== false ? "Désactiver" : "Activer"}
                            style={{ background: f.enabled !== false ? "var(--accent)" : "var(--border-strong)" }}>
                      <span className="absolute top-0.5 w-3 h-3 bg-white rounded-full transition-all" style={{ left: f.enabled !== false ? 14 : 2 }}/>
                    </button>
                  </td>
                  <td>
                    <div className="flex items-center gap-2.5">
                      <span className="inline-flex items-center justify-center w-6 h-6 rounded-md sunken border shrink-0" style={{ borderColor: "var(--border)" }}>
                        <Icon name={feedIcon(f.type)} size={13} className="text-subtle"/>
                      </span>
                      <span className="font-medium">{f.label}</span>
                      <span className="text-subtle text-[11px] mono">{f.type}</span>
                    </div>
                  </td>
                  <td>
                    {infoOnly ? (
                      <span className="text-[11.5px] text-subtle">Chargées automatiquement</span>
                    ) : f.url ? (
                      <span className="mono text-[11px] text-muted truncate inline-block max-w-[280px]" title={f.url}>{f.url}</span>
                    ) : (
                      <span className="text-[11.5px] text-amber-600">URL non configurée</span>
                    )}
                  </td>
                  <td className="text-muted text-[12px]">{infoOnly ? "—" : scheduleLabel(f.schedule)}</td>
                  <td>
                    {infoOnly ? <Badge tone="success" dot>OK</Badge>
                      : !ready ? <Badge tone="muted">Bientôt disponible</Badge>
                      : status === "running" ? <Badge tone="info" dot>En cours</Badge>
                      : status === "warning" || status === "partial_success" ? <Badge tone="warning" dot>Avertissement</Badge>
                      : status === "failed" ? <Badge tone="danger" dot>Échec</Badge>
                      : status === "success" ? <Badge tone="success" dot>OK</Badge>
                      : <Badge tone="muted">Jamais lancé</Badge>}
                  </td>
                  <td className="mono text-[12px] text-muted">{lastTs ? fmt.date(lastTs) : "—"}</td>
                  <td className="text-right mono tabular-nums">{lastRows != null ? fmt.num(lastRows) : "—"}</td>
                  <td className="text-right">
                    <div className="inline-flex items-center gap-1">
                      {!infoOnly && (
                      <Button size="sm" variant="primary"
                              leftIcon={runningType === f.type ? "spinner" : "download"}
                              disabled={!f.url || f.enabled === false || supplier.enabled === false || runningType != null || !ready}
                              title={
                                supplier.enabled === false ? "Imports désactivés pour ce fournisseur"
                                : !ready ? "Parser pas encore implémenté — URL enregistrée pour plus tard"
                                : !f.url ? "URL requise"
                                : f.enabled === false ? "Flux désactivé"
                                : "Prévisualiser puis importer"
                              }
                              onClick={() => runFeed(f)}>
                        {runningType === f.type ? "Téléchargement…" : "Importer"}
                      </Button>
                      )}
                      <IconButton icon="edit" size="sm" title="Modifier" onClick={() => setEditFeed(f)}/>
                      <IconButton icon="trash" size="sm" title="Retirer" onClick={() => setRemoveFeed(f)}/>
                    </div>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </Card>

      {feeds.some((f) => !f.url && !fluxMeta(f.type).infoOnly) && (
        <div className="mt-3 sunken rounded-md p-3 text-[12px] text-amber-700 dark:text-amber-300 flex items-start gap-2">
          <Icon name="warning" size={14} className="mt-0.5 shrink-0"/>
          <div>Certains flux n'ont pas d'URL configurée et ne peuvent pas être lancés. Cliquez sur l'icône d'édition pour renseigner l'URL.</div>
        </div>
      )}

      <FeedFormModal open={addOpen} onClose={() => setAddOpen(false)} available={available}
                     supplierId={supplier.id} toast={toast} mode="create"/>
      <FeedFormModal open={!!editFeed} onClose={() => setEditFeed(null)} available={available}
                     supplierId={supplier.id} toast={toast} mode="edit" feed={editFeed}/>

      <Modal open={!!removeFeed} onClose={() => setRemoveFeed(null)}
             title="Retirer ce flux ?"
             footer={<>
               <Button variant="ghost" onClick={() => setRemoveFeed(null)}>Annuler</Button>
               <Button variant="danger" leftIcon="trash" onClick={() => {
                 if (removeFeed) {
                   window.AxluStore.dispatch("supplier.removeFeed", { id: supplier.id, type: removeFeed.type });
                   toast(`Flux ${removeFeed.label} retiré`, { tone: "warning" });
                 }
                 setRemoveFeed(null);
               }}>Retirer</Button>
             </>}>
        {removeFeed && <div className="text-[13px]">Le flux <strong>{removeFeed.label}</strong> ne sera plus synchronisé. L'historique reste consultable.</div>}
      </Modal>

      <FeedPreviewModal preview={preview} onClose={() => setPreview(null)}
                        onImportSample={importPreviewed} onImportAll={importAll}/>
    </div>
  );
}

// ─── Feed preview modal: show 10 items normalized, confirm before full import ───
function FeedPreviewModal({ preview, onClose, onImportSample, onImportAll }) {
  if (!preview) return null;
  const { feed, items, totalAvailable, meta, fetchMs, bytes } = preview;
  const isProducts = feed.type === "products" || feed.type === "products_usb";

  const unit = isProducts ? "produits"
    : feed.type === "attributes" ? "attributs"
    : (feed.type === "print_prices" || feed.type === "print_prices_usb") ? "codes"
    : "entrées";

  // Reference-table feeds (print prices, attributes) must be imported whole —
  // a 10-row sample produces a broken partial table. Hide the sample button.
  const isReferenceFeed = feed.type === "print_prices" || feed.type === "print_prices_usb" || feed.type === "attributes";

  return (
    <Modal open={true} onClose={onClose} title={`Prévisualisation — ${flowLabel(feed.type)}`} width={1100}
           footer={<>
             <Button variant="ghost" onClick={onClose}>Annuler</Button>
             {!isReferenceFeed && (
               <Button variant="secondary" leftIcon="download" onClick={onImportSample}>
                 Importer ces {items.length} (test)
               </Button>
             )}
             <Button variant="primary" leftIcon="download" onClick={onImportAll}>
               Importer tout ({fmt.num(totalAvailable)} {unit})
             </Button>
           </>}>
        <div className="space-y-4">
          <div className="grid grid-cols-4 gap-3 text-[12px]">
            <div className="sunken rounded p-2.5">
              <div className="text-subtle text-[10.5px] uppercase tracking-wide">Source</div>
              <div className="font-medium mt-0.5 truncate" title={feed.url}>{feed.url}</div>
            </div>
            <div className="sunken rounded p-2.5">
              <div className="text-subtle text-[10.5px] uppercase tracking-wide">Total dispo</div>
              <div className="font-medium mt-0.5 mono tabular-nums">{fmt.num(totalAvailable)} {unit}</div>
            </div>
            <div className="sunken rounded p-2.5">
              <div className="text-subtle text-[10.5px] uppercase tracking-wide">Téléchargé</div>
              <div className="font-medium mt-0.5 mono">{((bytes || 0) / 1024 / 1024).toFixed(1)} Mo en {((fetchMs || 0) / 1000).toFixed(1)}s</div>
            </div>
            <div className="sunken rounded p-2.5">
              <div className="text-subtle text-[10.5px] uppercase tracking-wide">Généré par PF</div>
              <div className="font-medium mt-0.5 mono text-[11px]">{meta && meta.creationDateTime ? meta.creationDateTime : "—"}</div>
            </div>
          </div>

          <div className="text-[12px] text-muted">
            Aperçu des <strong>{items.length} premières {unit}</strong>. Vérifiez puis importez l'échantillon (test) ou la totalité.
          </div>

          <Card padding="none">
            <div style={{ maxHeight: 460, overflow: "auto" }}>
              <FeedPreviewTable type={feed.type} items={items}/>
            </div>
          </Card>

          {isProducts && (
            <div className="sunken rounded-md p-3 text-[11.5px] text-muted space-y-1">
              <div className="font-medium text-app">À noter avant l'import complet :</div>
              <ul className="list-disc pl-5 space-y-0.5">
                <li>L'import récupère <strong>{fmt.num(totalAvailable)} produits</strong> — comptez 30 à 60 secondes.</li>
                <li>Les <strong>prix et stocks restent à 0</strong> jusqu'à l'import des flux Prix et Stocks correspondants.</li>
                <li>Les <strong>catégories PF</strong> sont conservées en code natif ; mappez-les via l'écran <em>Catégories</em>.</li>
                <li>Re-importer ce flux <strong>met à jour</strong> les produits existants (dédup par SKU), sans doublon.</li>
              </ul>
            </div>
          )}
          {feed.type === "stocks" && (
            <div className="sunken rounded-md p-3 text-[11.5px] text-muted">
              L'import du stock <strong>met à jour les variantes existantes</strong> par SKU. Les SKU sans produit dans le catalogue sont ignorés (comptés à part).
            </div>
          )}
          {(feed.type === "prices" || feed.type === "prices_usb" || feed.type === "prices_ws") && (
            <div className="sunken rounded-md p-3 text-[11.5px] text-muted">
              Chaque variante reçoit ses <strong>paliers de prix d'achat PF</strong>. Les SKU sans produit existant sont ignorés.
            </div>
          )}
          {isReferenceFeed && (
            <div className="sunken rounded-md p-3 text-[11.5px] text-amber-700 dark:text-amber-300 flex items-start gap-2">
              <Icon name="warning" size={14} className="mt-0.5 shrink-0"/>
              <div>Donnée de <strong>référence</strong> — à importer <strong>en entier</strong> ({fmt.num(totalAvailable)} {unit}). Un import partiel rendrait le barème incomplet. C'est pourquoi seul « Importer tout » est proposé.</div>
            </div>
          )}
        </div>
    </Modal>
  );
}

// Type-aware preview table for the feed preview modal.
function FeedPreviewTable({ type, items }) {
  if (type === "products" || type === "products_usb") {
    return (
      <table className="axlu-table">
        <thead><tr>
          <th>Image</th><th>SKU</th><th>Titre</th><th>Marque</th><th>Catégorie PF</th>
          <th className="text-right">Poids</th><th>EAN</th><th>Marquage</th><th className="text-right">Variantes</th>
        </tr></thead>
        <tbody>
          {items.map((it, i) => (
            <tr key={it.sku || i} className="hover-row">
              <td>
                {it.imageUrl ? (
                  <img src={it.imageUrl} alt="" style={{ width: 36, height: 36, objectFit: "contain", border: "1px solid var(--border)", borderRadius: 4, background: "#fff" }}
                       onError={(e) => { e.currentTarget.style.opacity = 0.3; }}/>
                ) : <div style={{ width: 36, height: 36, background: "var(--surface-2)", borderRadius: 4 }}/>}
              </td>
              <td className="mono text-[11.5px]">{it.sku}</td>
              <td><div className="text-[12.5px] font-medium truncate max-w-[260px]" title={it.title}>{it.title}</div></td>
              <td className="text-[12px]">{it.brand || "—"}</td>
              <td>
                <div className="text-[12px]">{it.pfGroupDesc || "—"}</div>
                <div className="text-[10.5px] text-subtle mono">{it.pfCatDesc || "—"}</div>
              </td>
              <td className="text-right mono tabular-nums text-[12px]">{it.weight ? fmt.num(it.weight) + " g" : "—"}</td>
              <td className="mono text-[11px] text-muted">{it.ean || "—"}</td>
              <td className="text-[12px]">{it.marking && it.marking.method ? `${it.marking.method}${it.marking.maxColors ? " · " + it.marking.maxColors + "c" : ""}` : "—"}</td>
              <td className="text-right mono tabular-nums">{it.variantCount || (it.variants || []).length}</td>
            </tr>
          ))}
        </tbody>
      </table>
    );
  }
  if (type === "stocks") {
    return (
      <table className="axlu-table">
        <thead><tr>
          <th>SKU</th><th className="text-right">Stock direct</th><th className="text-right">Prochain réappro</th>
          <th>Date réappro</th><th className="text-right">Stock futur</th><th>Localisation</th>
        </tr></thead>
        <tbody>
          {items.map((it, i) => (
            <tr key={it.sku || i} className="hover-row">
              <td className="mono text-[11.5px]">{it.sku}</td>
              <td className="text-right mono tabular-nums">{fmt.num(it.stockDirect)}</td>
              <td className="text-right mono tabular-nums">{fmt.num(it.stockNextPo)}</td>
              <td className="mono text-[12px] text-muted">{it.stockDateNextPo || "—"}</td>
              <td className="text-right mono tabular-nums text-muted">{fmt.num(it.stockFuture)}</td>
              <td className="text-[12px]">{it.stockLocation || "—"}</td>
            </tr>
          ))}
        </tbody>
      </table>
    );
  }
  if (type === "prices" || type === "prices_usb" || type === "prices_ws") {
    return (
      <table className="axlu-table">
        <thead><tr>
          <th>SKU</th><th>Devise</th><th>Paliers d'achat (qté : prix net)</th><th>Prix conseillé PF</th><th className="text-right">Min. déco</th>
        </tr></thead>
        <tbody>
          {items.map((it, i) => (
            <tr key={it.sku || i} className="hover-row">
              <td className="mono text-[11.5px]">{it.sku}</td>
              <td className="text-[12px]">{it.currency || "—"}</td>
              <td className="mono text-[11.5px]">{(it.tiers || []).map((t) => `${t.fromQty}+ : ${fmt.eur(t.netPrice)}`).join("   ") || "—"}</td>
              <td className="mono text-[11.5px] text-muted">{(it.tiers || []).map((t) => fmt.eur(t.industryPrice)).join("  ") || "—"}</td>
              <td className="text-right mono tabular-nums">{it.minDecoQty || "—"}</td>
            </tr>
          ))}
        </tbody>
      </table>
    );
  }
  if (type === "print_data" || type === "print_data_ws" || type === "print_data_label") {
    return (
      <table className="axlu-table">
        <thead><tr>
          <th>SKU</th><th className="text-right">Méthodes</th><th>Détail des techniques d'impression</th>
        </tr></thead>
        <tbody>
          {items.map((it, i) => (
            <tr key={it.sku || i} className="hover-row">
              <td className="mono text-[11.5px]">{it.sku}</td>
              <td className="text-right mono tabular-nums">{(it.printMethods || []).length}</td>
              <td className="text-[11.5px]">
                {(it.printMethods || []).map((m, j) => (
                  <span key={j} className="inline-block mr-2 mb-0.5 px-1.5 py-0.5 sunken rounded">
                    {m.printCode} · {m.method}{m.isDefault ? " ★" : ""}
                  </span>
                ))}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    );
  }
  if (type === "print_prices" || type === "print_prices_usb") {
    return (
      <table className="axlu-table">
        <thead><tr>
          <th>Code</th><th>Méthode</th><th className="text-right">Frais LTM</th><th>Dépendance prix</th><th className="text-right">Combinaisons</th>
        </tr></thead>
        <tbody>
          {items.map((it, i) => (
            <tr key={it.printCode || i} className="hover-row">
              <td className="mono text-[12px] font-medium">{it.printCode}</td>
              <td className="text-[12px]">{it.method || "—"}</td>
              <td className="text-right mono tabular-nums">{fmt.eur(it.ltmCharge)}</td>
              <td className="text-[12px]">{it.priceDependence || "—"}</td>
              <td className="text-right mono tabular-nums">{(it.priceMatrix || []).length}</td>
            </tr>
          ))}
        </tbody>
      </table>
    );
  }
  if (type === "attributes") {
    return (
      <table className="axlu-table">
        <thead><tr><th>Code</th><th>Description</th><th>Infobulle</th></tr></thead>
        <tbody>
          {items.map((it, i) => (
            <tr key={it.code || i} className="hover-row">
              <td className="mono text-[12px]">{it.code}</td>
              <td className="text-[12.5px]">{it.description || "—"}</td>
              <td className="text-[12px] text-muted">{it.tooltip || "—"}</td>
            </tr>
          ))}
        </tbody>
      </table>
    );
  }
  return <div className="p-4 text-[12px] text-muted">Aperçu non disponible pour ce type.</div>;
}

function FeedFormModal({ open, onClose, available, supplierId, toast, mode, feed }) {
  // mode: "create" | "edit"
  const isEdit = mode === "edit";
  const initialType = isEdit ? (feed && feed.type) : (available[0]?.id || "");
  const [type, setType] = useState(initialType);
  const [url, setUrl] = useState(isEdit ? (feed && feed.url) || "" : (fluxMeta(initialType).defaultUrl || ""));
  const [schedule, setSchedule] = useState(isEdit ? (feed && feed.schedule) || "manual" : "manual");

  React.useEffect(() => {
    if (!open) return;
    if (isEdit && feed) {
      setType(feed.type);
      setUrl(feed.url || "");
      setSchedule(feed.schedule || "manual");
    } else {
      const first = available[0]?.id || "";
      setType(first);
      setUrl(first ? (fluxMeta(first).defaultUrl || "") : "");
      setSchedule(first ? fluxMeta(first).presetSchedule : "manual");
    }
  }, [open, isEdit, feed && feed.type, available.length]);

  const onTypeChange = (newType) => {
    setType(newType);
    if (!isEdit) {
      // Auto-suggest the PF Concept default cadence + known feed URL.
      setSchedule(fluxMeta(newType).presetSchedule);
      setUrl(fluxMeta(newType).defaultUrl || "");
    }
  };

  const submit = () => {
    if (isEdit) {
      window.AxluStore.dispatch("supplier.updateFeed", {
        id: supplierId, type: feed.type,
        patch: { url: url.trim(), schedule },
      });
      toast("Flux mis à jour", { tone: "success" });
    } else {
      if (!type) return;
      window.AxluStore.dispatch("supplier.addFeed", {
        id: supplierId,
        feed: { type, url: url.trim(), schedule, enabled: true },
      });
      toast("Flux ajouté", { tone: "success" });
    }
    onClose();
  };

  if (!isEdit && available.length === 0 && open) {
    return (
      <Modal open={open} onClose={onClose} title="Ajouter un flux" width={460}
             footer={<Button variant="primary" onClick={onClose}>Fermer</Button>}>
        <div className="text-[13px] text-muted">Tous les flux sont déjà configurés pour ce fournisseur.</div>
      </Modal>
    );
  }

  const typeOptions = isEdit
    ? [{ value: feed?.type || "", label: fluxMeta(feed?.type || "").label }]
    : available.map((f) => ({ value: f.id, label: f.label }));

  return (
    <Modal open={open} onClose={onClose} title={isEdit ? "Modifier le flux" : "Ajouter un flux"} width={520}
           footer={<>
             <Button variant="ghost" onClick={onClose}>Annuler</Button>
             <Button variant="primary" onClick={submit} disabled={!type}>{isEdit ? "Enregistrer" : "Ajouter"}</Button>
           </>}>
      <div className="space-y-3">
        <Field label="Type de flux">
          {isEdit ? (
            <Input value={fluxMeta(type).label + " (" + type + ")"} disabled className="opacity-70"/>
          ) : (
            <Select value={type} onChange={onTypeChange} options={typeOptions}/>
          )}
        </Field>
        <Field label="URL du flux">
          <Input value={url} onChange={(e) => setUrl(e.target.value)} placeholder="https://…" className="mono text-[12px]"/>
          <div className="text-[11px] text-subtle mt-1">URL complète vers le fichier JSON ou XML exposé par le fournisseur.</div>
        </Field>
        <Field label="Cadence des synchronisations">
          <ScheduleEditor value={schedule} onChange={setSchedule}/>
          <div className="text-[11px] text-subtle mt-1">Réglez librement les jours et heures de synchronisation automatique de ce flux.</div>
        </Field>
      </div>
    </Modal>
  );
}

function feedIcon(t) {
  return ({
    products: "cube", products_usb: "cube",
    prices: "coin", prices_usb: "coin", prices_ws: "coin",
    stocks: "catalog",
    marking: "tag", print_data: "tag", print_data_ws: "tag", print_data_label: "tag",
    print_prices: "coin", print_prices_usb: "coin",
    attributes: "log", images: "image",
  })[t] || "log";
}

function SupplierRulesTab({ supplier, navigate }) {
  const v = useAxluStore();
  const toast = useToast();
  const [excluded, setExcluded] = useState(supplier.excludedCategories || []);
  const [confirmDelete, setConfirmDelete] = useState(false);
  const [confirmDisable, setConfirmDisable] = useState(false);

  const patchSupplier = (p) => window.AxluStore.dispatch("supplier.update", { id: supplier.id, patch: p });

  const removeExcluded = (id) => {
    const next = excluded.filter((x) => x !== id);
    setExcluded(next);
    patchSupplier({ excludedCategories: next });
  };

  const doDisable = () => {
    window.AxluStore.dispatch("supplier.toggleEnabled", { id: supplier.id });
    toast(supplier.enabled === false ? "Imports activés" : "Imports désactivés", { tone: "warning" });
    setConfirmDisable(false);
  };

  const doDelete = () => {
    window.AxluStore.dispatch("supplier.delete", { id: supplier.id });
    toast(`Fournisseur « ${supplier.name} » supprimé`, { tone: "danger" });
    setConfirmDelete(false);
    navigate("#/suppliers");
  };

  const R = supplier.rules || {};

  return (
    <div className="grid grid-cols-2 gap-6">
      <Card title="Contrôles qualité à l'import" className="col-span-2">
        <div className="text-[12.5px] text-muted mb-2">
          Quand une de ces conditions est remplie lors d'un import, le produit est marqué <strong>« à vérifier »</strong> au lieu d'être approuvé — pour qu'un humain le contrôle avant publication.
        </div>
        <div className="space-y-2">
          <RuleToggle label="Mettre à vérifier si le produit n'a pas de code EAN" supplierId={supplier.id} ruleKey="reviewNoEan" defaultOn={R.reviewNoEan ?? true}/>
          <RuleToggle label="Mettre à vérifier si le produit n'a pas d'image" supplierId={supplier.id} ruleKey="reviewNoImage" defaultOn={R.reviewNoImage ?? true}/>
          <RuleToggle label="Mettre à vérifier si le titre change de plus de 30 %" supplierId={supplier.id} ruleKey="reviewTitleChange" defaultOn={R.reviewTitleChange ?? true}/>
          <RuleToggle label="Mettre à vérifier si le produit n'est mappé à aucune catégorie AXLU" supplierId={supplier.id} ruleKey="reviewNoCategory" defaultOn={R.reviewNoCategory ?? true}/>
          <RuleToggle label="Mettre à vérifier si le stock du produit n'est pas en Europe" supplierId={supplier.id} ruleKey="reviewStockOutsideEu" defaultOn={R.reviewStockOutsideEu ?? true}/>
          <RuleToggle label="Mettre à vérifier si le prix de vente est inférieur à (prix d'achat ÷ 0,7)" supplierId={supplier.id} ruleKey="reviewLowPrice" defaultOn={R.reviewLowPrice ?? true}/>
          <RuleToggle label="Mettre à vérifier si le prix d'achat augmente de plus de 10 %" supplierId={supplier.id} ruleKey="reviewPriceUp" defaultOn={R.reviewPriceUp ?? true}/>
        </div>
      </Card>

      <Card title="Catégories exclues de l'import" className="col-span-2">
        <ExcludedCategoriesField
          excluded={excluded}
          onAdd={(id) => {
            if (!id || excluded.includes(id)) return;
            const next = [...excluded, id];
            setExcluded(next);
            patchSupplier({ excludedCategories: next });
          }}
          onRemove={removeExcluded}
        />
      </Card>

      <Card title="Zone dangereuse" className="col-span-2" padding="md">
        <div className="flex gap-2">
          <Button variant="secondary" onClick={() => setConfirmDisable(true)}>{supplier.enabled === false ? "Réactiver les imports" : "Désactiver les imports"}</Button>
          <Button variant="danger" disabled={!window.AxluStore.can("delete")} onClick={() => setConfirmDelete(true)}>Supprimer ce fournisseur</Button>
        </div>
        <div className="text-[11px] text-subtle mt-2">La suppression conserve les produits déjà importés mais coupe la synchronisation.</div>
      </Card>

      <Modal open={confirmDisable} onClose={() => setConfirmDisable(false)} title={supplier.enabled === false ? "Réactiver les imports ?" : "Désactiver les imports ?"} width={420}
             footer={<>
               <Button variant="ghost" onClick={() => setConfirmDisable(false)}>Annuler</Button>
               <Button variant="primary" onClick={doDisable}>Confirmer</Button>
             </>}>
        <div className="text-[13px]">
          {supplier.enabled === false
            ? `Réactiver les imports pour « ${supplier.name} » ?`
            : `Désactiver les imports pour « ${supplier.name} » ? Les synchros automatiques seront mises en pause.`}
        </div>
      </Modal>

      <Modal open={confirmDelete} onClose={() => setConfirmDelete(false)} title="Supprimer le fournisseur ?" width={460}
             footer={<>
               <Button variant="ghost" onClick={() => setConfirmDelete(false)}>Annuler</Button>
               <Button variant="danger" onClick={doDelete}>Supprimer définitivement</Button>
             </>}>
        <div className="text-[13px]">Supprimer définitivement « <span className="font-medium">{supplier.name}</span> » ? Cette action est irréversible.</div>
      </Modal>
    </div>
  );
}

function ExcludedCategoriesField({ excluded, onAdd, onRemove }) {
  const v = useAxluStore();
  const { CATEGORIES_AXLU, CATEGORIES_PF } = window.AxluData;
  const [draft, setDraft] = useState("");
  const [pickedAxlu, setPickedAxlu] = useState("");

  const submitDraft = () => {
    const val = draft.trim();
    if (!val) return;
    onAdd(val);
    setDraft("");
  };

  const axluOptions = (CATEGORIES_AXLU || [])
    .filter((c) => !excluded.includes(c.id))
    .map((c) => ({ value: c.id, label: `${c.name} (${c.id})` }));

  return (
    <div>
      <label className="block text-[12px] font-medium text-muted mb-1.5">Catégories exclues</label>

      {excluded.length > 0 ? (
        <div className="flex flex-wrap gap-1.5 p-2 surface border rounded-md mb-2" style={{ borderColor: "var(--border)" }}>
          {excluded.map((id) => {
            const cat = (CATEGORIES_AXLU || []).find((c) => c.id === id)
                     || (CATEGORIES_PF || []).find((c) => c.id === id);
            const label = cat ? cat.name : id;
            return (
              <span key={id} className="inline-flex items-center gap-1 px-2 py-0.5 sunken rounded text-[12px]">
                <span>{label}</span>
                <span className="mono text-[10.5px] text-subtle">{id}</span>
                <button type="button" onClick={() => onRemove(id)} className="text-subtle hover:text-app ml-0.5" title="Retirer">
                  <Icon name="x" size={10}/>
                </button>
              </span>
            );
          })}
        </div>
      ) : (
        <div className="text-[12px] text-subtle p-2 sunken rounded-md mb-2">Aucune catégorie exclue.</div>
      )}

      <div className="grid grid-cols-2 gap-2 mt-2">
        {axluOptions.length > 0 && (
          <div>
            <label className="block text-[11px] text-subtle mb-1">Depuis le catalogue AXLU</label>
            <div className="flex gap-1.5">
              <Select value={pickedAxlu} onChange={setPickedAxlu}
                      options={[{ value: "", label: "— Choisir —" }, ...axluOptions]}/>
              <Button size="sm" variant="secondary" leftIcon="plus"
                      disabled={!pickedAxlu}
                      onClick={() => { onAdd(pickedAxlu); setPickedAxlu(""); }}>
                Ajouter
              </Button>
            </div>
          </div>
        )}
        <div className={axluOptions.length > 0 ? "" : "col-span-2"}>
          <label className="block text-[11px] text-subtle mb-1">Code libre (catégorie fournisseur)</label>
          <div className="flex gap-1.5">
            <Input value={draft}
                   onChange={(e) => setDraft(e.target.value)}
                   onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); submitDraft(); } }}
                   placeholder="ex. mc4, sc45, pf-stress…"
                   className="mono text-[12px]"/>
            <Button size="sm" variant="secondary" leftIcon="plus"
                    disabled={!draft.trim()}
                    onClick={submitDraft}>
              Ajouter
            </Button>
          </div>
        </div>
      </div>

      <div className="text-[11px] text-subtle mt-2">Les produits dont la catégorie correspond à un de ces codes ne seront pas importés.</div>
    </div>
  );
}

function RuleToggle({ label, defaultOn, supplierId, ruleKey, onChange }) {
  const [on, setOn] = useState(defaultOn);
  const toggle = () => {
    const next = !on;
    setOn(next);
    if (supplierId && ruleKey) {
      const cur = (window.AxluData.SUPPLIERS.find((s) => s.id === supplierId) || {}).rules || {};
      window.AxluStore.dispatch("supplier.update", {
        id: supplierId,
        patch: { rules: { ...cur, [ruleKey]: next } },
      });
    }
    if (typeof onChange === "function") onChange(next);
  };
  return (
    <div className="flex items-center justify-between py-2 border-b last:border-b-0" style={{ borderColor: "var(--border)" }}>
      <div className="text-[13px]">{label}</div>
      <button onClick={toggle}
        className={`w-9 h-5 rounded-full relative transition-colors`}
        style={{ background: on ? "var(--accent)" : "var(--border-strong)" }}>
        <span className={`absolute top-0.5 w-4 h-4 bg-white rounded-full transition-all`} style={{ left: on ? 18 : 2 }}/>
      </button>
    </div>
  );
}

function SupplierHistoryTab({ syncs, navigate }) {
  const v = useAxluStore();
  const { SYNC_STATUSES } = window.AxluData;
  const [statusFilter, setStatusFilter] = useState("all");
  const [typeFilter, setTypeFilter] = useState("all");
  const [expandedDays, setExpandedDays] = useState(null); // null = default (most recent open)

  // Sort by most recent first.
  const sorted = useMemo(() => {
    return [...syncs].sort((a, b) => new Date(b.startedAt) - new Date(a.startedAt));
  }, [v, syncs]);

  const filtered = sorted.filter((s) =>
    (statusFilter === "all" || s.status === statusFilter) &&
    (typeFilter === "all" || s.type === typeFilter)
  );

  const fluxTypes = [...new Set(sorted.map((s) => s.type))];
  const statusOptions = [
    { value: "all",            label: "Tous statuts" },
    { value: "success",        label: "Succès" },
    { value: "partial_success",label: "Succès partiel" },
    { value: "failed",         label: "Échec" },
    { value: "interrupted",    label: "Interrompue" },
    { value: "running",        label: "En cours" },
  ];

  if (sorted.length === 0) {
    return (
      <Card padding="lg">
        <EmptyState icon="clock" title="Aucun historique de synchro"
                    description="Lancez un flux pour voir apparaître ici toutes les exécutions, leur statut et leurs erreurs."/>
      </Card>
    );
  }

  // Group by date (yyyy-mm-dd) for daily breakdown.
  const groupedByDay = filtered.reduce((acc, s) => {
    const day = (s.startedAt || "").slice(0, 10);
    if (!acc[day]) acc[day] = [];
    acc[day].push(s);
    return acc;
  }, {});
  const days = Object.keys(groupedByDay).sort((a, b) => b.localeCompare(a));
  // Collapsible days — the most recent day is open by default until the user
  // interacts; then `expandedDays` holds the exact open set.
  const openSet = expandedDays || new Set(days.slice(0, 1));
  const toggleDay = (day) => {
    const next = new Set(openSet);
    if (next.has(day)) next.delete(day); else next.add(day);
    setExpandedDays(next);
  };

  const errorMessage = (s) => {
    if (s.errorMsg) return s.errorMsg;
    if (s.errorMessage) return s.errorMessage;
    if (s.error) return s.error;
    if (s.status === "failed") return "Échec sans message détaillé";
    if (s.status === "interrupted") return "Synchronisation interrompue (application fermée)";
    if (s.status === "partial_success") return `${s.errors || 0} ligne${(s.errors || 0) > 1 ? "s" : ""} en erreur`;
    return null;
  };

  return (
    <div className="space-y-4">
      <div className="flex items-center gap-2">
        <Select value={statusFilter} onChange={setStatusFilter} options={statusOptions}/>
        <Select value={typeFilter} onChange={setTypeFilter} options={[
          { value: "all", label: "Tous flux" },
          ...fluxTypes.map((t) => ({ value: t, label: flowLabel(t) })),
        ]}/>
        <div className="flex-1"/>
        <span className="text-[12px] text-muted">{filtered.length} entrée{filtered.length > 1 ? "s" : ""}</span>
      </div>

      {days.map((day) => {
        const list = groupedByDay[day];
        const okCount = list.filter((s) => s.status === "success").length;
        const total = list.length;
        const allOk = okCount === total;
        const open = openSet.has(day);
        const dayLabel = day === "" ? "(date inconnue)"
          : new Date(day).toLocaleDateString("fr-FR", { weekday: "long", day: "2-digit", month: "long", year: "numeric" });
        return (
        <Card key={day} padding="none">
          <button onClick={() => toggleDay(day)}
                  className="w-full flex items-center gap-3 px-4 py-3 hover-row text-left">
            <Icon name={open ? "chevronDown" : "chevronRight"} size={14} className="text-subtle shrink-0"/>
            <span className="font-medium text-[13px] flex-1 capitalize">{dayLabel}</span>
            <span className={`flex items-center gap-1.5 text-[12px] mono tabular-nums ${allOk ? "text-muted" : "text-amber-600"}`}
                  title={`${okCount} synchro${okCount > 1 ? "s" : ""} réussie${okCount > 1 ? "s" : ""} sur ${total}`}>
              {!allOk && <Icon name="warning" size={13}/>}
              {okCount}/{total}
            </span>
          </button>
          {open && (
          <table className="axlu-table">
            <thead>
              <tr>
                <th>Heure</th>
                <th>Flux</th>
                <th>Statut</th>
                <th className="text-right">Lignes</th>
                <th className="text-right">Erreurs</th>
                <th>Durée</th>
                <th>Détail</th>
              </tr>
            </thead>
            <tbody>
              {list.map((s) => {
                const err = errorMessage(s);
                return (
                  <tr key={s.id} className={s.status === "failed" ? "hover-row" : "hover-row"}>
                    <td className="mono text-[12px] tabular-nums">{s.startedAt ? new Date(s.startedAt).toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit", second: "2-digit" }) : "—"}</td>
                    <td>{flowLabel(s.type)}</td>
                    <td><StatusBadge status={s.status} map={SYNC_STATUSES}/></td>
                    <td className="text-right mono tabular-nums">{fmt.num(s.rowsRead)}</td>
                    <td className="text-right">
                      {s.errors > 0 ? <Badge tone="danger">{s.errors}</Badge> : <span className="text-subtle">0</span>}
                    </td>
                    <td className="mono text-[12px] text-muted">{fmt.duration(s.duration)}</td>
                    <td>
                      {err ? (
                        <span className={`text-[12px] ${s.status === "failed" ? "text-red-600" : "text-amber-600"}`}>{err}</span>
                      ) : (
                        <span className="text-[11px] text-subtle">—</span>
                      )}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
          )}
        </Card>
        );
      })}
    </div>
  );
}

Object.assign(window, {
  LoginScreen, Dashboard, SuppliersList, SupplierDetail,
  StatusDot, flowLabel, actionLabel, Logo, Field, Row, RuleToggle, SupplierStatusBadge,
});
