// AXLU — screens 3: Product detail (9 tabs)

function ProductDetail({ productId, navigate }) {
  const v = useAxluStore();
  const { PRODUCTS, PRODUCT_STATUSES, CATEGORIES_AXLU } = window.AxluData;
  const productRaw = PRODUCTS.find((p) => p.id === productId);
  // WEB : un produit du bootstrap est « allégé » (sans tableau `variants`). Les
  // onglets détail (brut/variantes/images/marquage/prix/stock) ont besoin de
  // l'objet complet → on l'hydrate une fois depuis l'API à l'ouverture.
  const isWeb = !!window.__AXLU_WEB__;
  const needsHydration = isWeb && !!productRaw && !Array.isArray(productRaw.variants) && !productRaw.__pending;

  // Guard: product not found → redirect to catalog. Hooks below must still be
  // declared (rules of hooks), so we use a safe fallback shape.
  React.useEffect(() => {
    if (!productRaw) navigate("#/catalog");
  }, [productRaw]);

  // Normalize shape: old & new products may be missing some fields. Apply
  // defaults so the render never crashes.
  const product = productRaw ? {
    ...productRaw,
    dimensions: productRaw.dimensions || { l: 0, w: 0, h: 0, d: 0 },
    variants: Array.isArray(productRaw.variants) ? productRaw.variants : [],
    marking: { zones: [], methods: [], ...(productRaw.marking || {}) },
    imageGallery: productRaw.imageGallery || [],
  } : null;

  const [tab, setTab] = useState("data");
  const [hydrating, setHydrating] = useState(needsHydration);
  const [edited, setEdited] = useState(() => product ? {
    title: product.title || '',
    description: product.description || '',
    categoryIds: Array.isArray(product.categoryIds) && product.categoryIds.length
      ? product.categoryIds
      : (product.categoryId ? [product.categoryId] : []),
    brand: product.brand || '',
    material: product.material || '',
    color: product.primaryColor || "Multi",
    weight: product.weight || 0,
    dimL: product.dimensions.l, dimW: product.dimensions.w,
    dimH: product.dimensions.h, dimD: product.dimensions.d || 0,
  } : {});
  const [manualPrice, setManualPrice] = useState(product ? product.manualPrice : null);
  const [shopifyModalOpen, setShopifyModalOpen] = useState(false);
  const [deleteOpen, setDeleteOpen] = useState(false);
  const [reloading, setReloading] = useState(false);
  const [publishing, setPublishing] = useState(false);
  const toast = useToast();
  const canDelete = window.AxluStore.can("delete");

  // WEB : hydratation à la demande — récupère l'objet COMPLET (variantes + détail)
  // via GET /api/products/:id et le fusionne en place dans window.AxluData.PRODUCTS.
  // Les scalaires du catalogue (statut/prix/Shopify) du « light » gagnent pour
  // rester cohérents avec la liste. Electron : le produit complet est déjà là → skip.
  React.useEffect(() => {
    if (!isWeb || !productRaw || Array.isArray(productRaw.variants) || productRaw.__pending) { setHydrating(false); return; }
    let cancelled = false;
    setHydrating(true);
    (async () => {
      try {
        const r = await fetch('/api/products/' + encodeURIComponent(productId), { credentials: 'same-origin' });
        if (!r.ok) throw new Error('HTTP ' + r.status);
        const full = await r.json();
        const list = window.AxluData.PRODUCTS;
        const idx = list.findIndex((p) => p.id === productId);
        if (idx !== -1) {
          const light = list[idx];
          list[idx] = {
            ...full, ...light,
            variants: Array.isArray(full.variants) ? full.variants : [],
            attributes: full.attributes || light.attributes || [],
            imageGallery: full.imageGallery || [],
            marking: full.marking || light.marking || {},
            dimensions: full.dimensions || light.dimensions || { l: 0, w: 0, h: 0, d: 0 },
            _full: true,
          };
        }
        if (!cancelled) { window.AxluStore.notify(); setHydrating(false); }
      } catch (e) {
        if (!cancelled) { try { toast("Détail produit indisponible — données partielles", { tone: "danger" }); } catch (_) {} setHydrating(false); }
      }
    })();
    return () => { cancelled = true; };
  }, [productId, isWeb, productRaw && Array.isArray(productRaw.variants)]);

  // WEB : quand l'objet complet est arrivé, resynchronise le formulaire d'édition
  // avec les champs « détail » (description, dimensions…) absents du catalogue allégé.
  React.useEffect(() => {
    if (!product || !product._full) return;
    setEdited({
      title: product.title || '',
      description: product.description || '',
      categoryIds: Array.isArray(product.categoryIds) && product.categoryIds.length
        ? product.categoryIds : (product.categoryId ? [product.categoryId] : []),
      brand: product.brand || '',
      material: product.material || '',
      color: product.primaryColor || "Multi",
      weight: product.weight || 0,
      dimL: product.dimensions.l, dimW: product.dimensions.w,
      dimH: product.dimensions.h, dimD: product.dimensions.d || 0,
    });
    setManualPrice(product.manualPrice);
  }, [product && product._full]);

  // Verrou collaboratif : on signale sa présence sur la fiche (1er arrivé = éditeur ; les autres en lecture seule).
  React.useEffect(() => {
    if (!product || !window.AxluStore.acquireLock) return;
    const res = 'product:' + product.id;
    window.AxluStore.acquireLock(res);
    return () => window.AxluStore.releaseLock(res);
  }, [product && product.id]);

  if (!product) return null;
  if (hydrating) {
    return <div className="p-6 text-muted" style={{ fontSize: 13 }}>Chargement du produit…</div>;
  }
  const status = product.status;
  const locked = window.AxluStore.lockedByOther ? window.AxluStore.lockedByOther('product:' + product.id) : null;

  const handleDelete = () => {
    const sku = product.sku;
    window.AxluStore.dispatch("product.delete", { id: product.id });
    setDeleteOpen(false);
    toast(`Produit ${sku} supprimé`, { tone: "danger" });
    navigate("#/catalog");
  };

  const tabs = [
    { id: "raw",       label: "Données brutes",     icon: "log" },
    { id: "data",      label: "Données AXLU",       icon: "edit" },
    { id: "variants",  label: "Variantes",          icon: "cube",  count: product.variants.length },
    { id: "images",    label: "Images",             icon: "image", count: (product.variants || []).reduce((s, vr) => s + (vr.imageUrl ? 1 : 0) + (vr.imageGallery || []).length, 0) },
    { id: "marking",   label: "Marquage",           icon: "tag" },
    { id: "pricing",   label: "Prix",               icon: "coin" },
    { id: "stock",     label: "Stock",              icon: "catalog" },
    { id: "shopify",   label: "Publication Shopify",icon: "cart" },
    { id: "history",   label: "Historique",         icon: "clock" },
  ];

  const setStatusAndToast = (s, label) => {
    window.AxluStore.dispatch('product.setStatus', { id: product.id, status: s });
    toast(`Produit ${label}`);
  };

  // Real Shopify publish (create or update) for this product.
  const handlePublishShopify = async () => {
    if (publishing) return;
    setPublishing(true);
    toast(`Publication de ${product.sku} sur Shopify…`, { tone: 'info' });
    try {
      const r = await window.AxluStore.shopifyPublish(product.id);
      if (r && r.ok) {
        const sw = r.stock && r.stock.ok === false ? ` · stock non poussé : ${r.stock.error || 'erreur'}` : '';
        toast(`${product.sku} publié sur Shopify ✓${sw}`, { tone: sw ? 'warning' : 'success' });
      } else {
        toast(`Échec : ${(r && r.error) || 'erreur inconnue'}`, { tone: 'danger' });
      }
    } catch (e) {
      toast(`Échec : ${e}`, { tone: 'danger' });
    } finally { setPublishing(false); }
  };

  // Activate on Shopify (publishes first if not yet on Shopify).
  const handleActivateShopify = async () => {
    if (publishing) return;
    setPublishing(true);
    try {
      const r = await window.AxluStore.shopifyActivate(product.id);
      if (r && r.ok) {
        toast(`${product.sku} activé sur Shopify ✓`, { tone: 'success' });
      } else {
        toast(`Échec : ${(r && r.error) || 'erreur inconnue'}`, { tone: 'danger' });
      }
    } catch (e) {
      toast(`Échec : ${e}`, { tone: 'danger' });
    } finally { setPublishing(false); }
  };

  // Deactivate on Shopify → product goes back to draft.
  const handleDeactivateShopify = async () => {
    if (publishing) return;
    setPublishing(true);
    try {
      const r = await window.AxluStore.shopifyDeactivate(product.id);
      if (r && r.ok) {
        toast(`${product.sku} repassé en brouillon`, { tone: 'warning' });
      } else {
        toast(`Échec : ${(r && r.error) || 'erreur inconnue'}`, { tone: 'danger' });
      }
    } catch (e) {
      toast(`Échec : ${e}`, { tone: 'danger' });
    } finally { setPublishing(false); }
  };

  // Unpublish = delete the product from Shopify entirely.
  const handleUnpublishShopify = async () => {
    if (publishing) return;
    setPublishing(true);
    try {
      const r = await window.AxluStore.shopifyUnpublish(product.id);
      if (r && r.ok) {
        toast(`${product.sku} retiré de Shopify`, { tone: 'warning' });
      } else {
        toast(`Échec : ${(r && r.error) || 'erreur inconnue'}`, { tone: 'danger' });
      }
    } catch (e) {
      toast(`Échec : ${e}`, { tone: 'danger' });
    } finally { setPublishing(false); }
  };

  // Reload this product's data straight from the supplier's feeds.
  const handleReload = async () => {
    if (reloading) return;
    if (typeof reloadProduct !== 'function') { toast('Rechargement indisponible', { tone: 'danger' }); return; }
    setReloading(true);
    toast('Rechargement du produit depuis les flux…', { tone: 'info' });
    try {
      const res = await reloadProduct(product, {
        toast,
        onProgress: (p) => toast(`Recharge ${p.index + 1}/${p.total} — ${p.label}…`, { tone: 'info' }),
      });
      toast(res.message, { tone: res.ok ? 'success' : 'warning' });
    } catch (e) {
      toast('Erreur : ' + (e.message || e), { tone: 'danger' });
    } finally {
      setReloading(false);
    }
  };

  const handleDuplicate = () => {
    const before = window.AxluData.PRODUCTS.length;
    window.AxluStore.dispatch('product.duplicate', { id: product.id });
    const after = window.AxluData.PRODUCTS;
    const newProd = after[0]?.id !== product.id ? after[0] : after.find(p => p.id !== product.id && p.sku?.includes(product.sku));
    toast('Produit dupliqué', { tone: 'success' });
    if (newProd) {
      navigate(`#/products/${newProd.id}`);
    }
  };

  const handleViewShopify = () => {
    if (product.shopifyId) {
      setShopifyModalOpen(true);
    } else {
      toast('Produit non publié', { tone: 'warning' });
    }
  };

  return (
    <div>
      <PageHeader
        onBack={() => navigate("#/catalog")}
        breadcrumb={[
          { label: "Catalogue", href: "#/catalog" },
          { label: product.categoryLabel, href: `#/catalog?cat=${product.categoryId}` },
          { label: product.sku }
        ]}
        title={
          <span className="flex items-center gap-3 min-w-0">
            <ProductThumb sku={product.sku} src={product.imageUrl} hasImage size={40} className="!rounded-md"/>
            <span className="truncate">{edited.title}</span>
            <StatusBadge status={status} map={PRODUCT_STATUSES}/>
            <ShopifyPresenceBadge pub={product.shopifyPub}/>
            <ShopifyStateBadge pub={product.shopifyPub}/>
            {product.duplicate && <Badge tone="warning">Doublon potentiel</Badge>}
          </span>
        }
        subtitle={
          <span className="flex items-center gap-3 mono text-[12px]">
            <span>{product.sku}</span>
            <span className="text-subtle">·</span>
            <span>{product.supplierName}</span>
            <span className="text-subtle">·</span>
            <span>{edited.brand}</span>
          </span>
        }
        actions={
          <>
            <Dropdown
              trigger={<Button variant="secondary" rightIcon="chevronDown">Actions</Button>}
              align="right" width={220}
              items={[
                { label: reloading ? "Rechargement…" : "Recharger depuis les flux", icon: "sync",
                  disabled: reloading, onClick: handleReload },
                { label: publishing ? "Publication…" : "Mettre à jour Shopify", icon: "refresh",
                  disabled: publishing, onClick: handlePublishShopify },
                { label: "Désactiver (brouillon)", icon: "eyeOff",
                  disabled: publishing || product.shopifyPub !== 'active', onClick: handleDeactivateShopify },
                { label: "Dépublier (retirer de Shopify)", icon: "trash", danger: true,
                  disabled: publishing || !product.shopifyId, onClick: handleUnpublishShopify },
                { label: "Dupliquer", icon: "edit", onClick: handleDuplicate },
                { label: "Voir sur Shopify", icon: "link", onClick: handleViewShopify },
                "sep",
                { label: "Archiver", icon: "trash", onClick: () => setStatusAndToast("archived", "archivé") },
                "sep",
                {
                  label: "Supprimer définitivement",
                  icon: "trash",
                  danger: true,
                  disabled: !canDelete,
                  onClick: () => setDeleteOpen(true),
                },
              ]}/>
            {status === "to_review" ? (
              <Button variant="secondary" leftIcon="cross" onClick={() => setStatusAndToast("blocked", "bloqué")}>Bloquer</Button>
            ) : null}
            {status !== "approved" && (
              <Button variant="secondary" leftIcon="check" onClick={() => setStatusAndToast("approved", "approuvé")}>Approuver</Button>
            )}
            {product.shopifyPub !== 'active' && (
              <Button variant="secondary" leftIcon="check" disabled={publishing} onClick={handleActivateShopify}>Activer</Button>
            )}
            <Button variant="primary" leftIcon="cart" disabled={publishing} onClick={handlePublishShopify}>
              {publishing ? "Publication…" : (product.shopifyId ? "Republier" : "Publier sur Shopify")}
            </Button>
          </>
        }
        tabs={<Tabs value={tab} onChange={setTab} tabs={tabs}/>}
      />

      <div className="p-6">
        {locked && (
          <div className="sunken rounded-md p-3 mb-4 text-[12px] text-red-700 dark:text-red-300 flex items-start gap-2">
            <span className="text-[14px] leading-none">🔒</span>
            <div><strong>En cours d'utilisation par {locked.userLabel || "un autre utilisateur"}.</strong> Lecture seule — tes modifications ne seront pas enregistrées tant qu'il/elle est sur la fiche.</div>
          </div>
        )}
        {(status === "to_review" || product.shopifyError) && (
          <div className="sunken rounded-md p-3 mb-4 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>
              <strong>À vérifier{product.shopifyPub === "draft" ? " — désactivé sur Shopify (brouillon)" : ""}.</strong>
              {Array.isArray(product.reviewReasons) && product.reviewReasons.length > 0 && (
                <> Éléments à surveiller : {product.reviewReasons.join(" · ")}.</>
              )}
              {product.shopifyError && <> {product.shopifyError}</>}
            </div>
          </div>
        )}
        {tab === "raw" && <ProductRawTab product={product}/>}
        {tab === "data" && <ProductDataTab product={product} edited={edited} setEdited={setEdited}/>}
        {tab === "variants" && <ProductVariantsTab product={product}/>}
        {tab === "images" && <ProductImagesTab product={product}/>}
        {tab === "marking" && <ProductMarkingTab product={product}/>}
        {tab === "pricing" && <ProductPricingTab product={product} manualPrice={manualPrice} setManualPrice={setManualPrice}/>}
        {tab === "stock" && <ProductStockTab product={product}/>}
        {tab === "shopify" && <ProductShopifyTab product={product} status={status}/>}
        {tab === "history" && <ProductHistoryTab product={product}/>}
      </div>

      {shopifyModalOpen && product.shopifyId && (
        <Modal open={shopifyModalOpen} onClose={() => setShopifyModalOpen(false)} title="Voir sur Shopify"
               footer={<Button variant="secondary" onClick={() => setShopifyModalOpen(false)}>Fermer</Button>}>
          <div className="space-y-3 text-[13px]">
            <div>Le produit sera ouvert dans l'admin Shopify.</div>
            <div className="sunken rounded p-2 mono text-[11px] break-all">
              https://admin.shopify.com/products/{product.shopifyId.split('/').pop()}
            </div>
            <Button variant="primary" leftIcon="link" onClick={() => {
              window.open(`https://admin.shopify.com/products/${product.shopifyId.split('/').pop()}`, '_blank');
              setShopifyModalOpen(false);
            }}>Ouvrir dans le navigateur</Button>
          </div>
        </Modal>
      )}

      <Modal open={deleteOpen} onClose={() => setDeleteOpen(false)}
             title="Supprimer ce produit ?"
             footer={<>
               <Button variant="ghost" onClick={() => setDeleteOpen(false)}>Annuler</Button>
               <Button variant="danger" leftIcon="trash" onClick={handleDelete}>Supprimer définitivement</Button>
             </>}>
        <div className="space-y-2 text-[13px]">
          <div>Supprimer définitivement <span className="font-medium">{product.sku}</span> — {product.title} ?</div>
          <div className="text-muted">Cette action est <strong>irréversible</strong>. Pour conserver l'historique, préférez « Archiver ».</div>
        </div>
      </Modal>
    </div>
  );
}

// ─── Raw data tab ──────────────────────────────────────────────────────
function ProductRawTab({ product }) {
  const [view, setView] = useState("table");
  const variants = product.variants || [];
  const [variantIdx, setVariantIdx] = useState(0);
  const activeVariant = variants[variantIdx] || variants[0] || {};

  // Model-level raw fields (description, attributes, etc.).
  const modelRaw = {
    modelCode: product.modelCode,
    title: product.descriptionShort || product.title,
    extendedDescription: product.description,
    keywords: product.keywords,
    productComments: product.productComments,
    brand: product.brand,
    categoryGroup: `${product.pfGroupCode || "—"} · ${product.pfGroupDesc || "—"}`,
    category: `${product.pfCatCode || "—"} · ${product.pfCatDesc || "—"}`,
    attributes: (product.attributes || []).map((a) => `${a.code}: ${a.value}`),
    variantCount: product.variantCount || variants.length,
  };

  // The full original PF item for the selected variant.
  const variantRaw = activeVariant.raw || activeVariant;

  return (
    <div className="space-y-4">
      <Card title="Données modèle (PF Concept)" padding="none">
        <table className="axlu-table">
          <tbody>
            {Object.entries(modelRaw).map(([k, val]) => (
              <tr key={k}>
                <td className="mono text-[12px] text-muted w-[220px]">{k}</td>
                <td className="text-[12px]">{Array.isArray(val) ? (val.length ? val.join("  ·  ") : "—") : (val == null || val === "" ? "—" : String(val))}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </Card>

      <Card title="Données brutes par variante" padding="none"
            action={
              <div className="flex items-center gap-2">
                <Badge tone="muted">Lecture seule · source PF</Badge>
                <div className="flex border rounded-md overflow-hidden surface" style={{ borderColor: "var(--border)" }}>
                  <button onClick={() => setView("table")} className={`px-2 py-1 text-[12px] ${view === "table" ? "accent-bg accent-fg" : "text-muted"}`}>Tableau</button>
                  <button onClick={() => setView("json")} className={`px-2 py-1 text-[12px] border-l ${view === "json" ? "accent-bg accent-fg" : "text-muted"}`} style={{ borderColor: "var(--border)" }}>JSON brut</button>
                </div>
              </div>
            }>
        {variants.length === 0 ? (
          <div className="p-4 text-[12px] text-muted">Aucune variante. Importez le flux Produits.</div>
        ) : (
          <>
            <div className="p-3 border-b flex items-center gap-2 flex-wrap" style={{ borderColor: "var(--border)" }}>
              <span className="text-[12px] text-muted">Variante :</span>
              {variants.map((vr, i) => (
                <button key={vr.sku || i} onClick={() => setVariantIdx(i)}
                        className={`px-2 py-0.5 text-[11px] mono rounded ${i === variantIdx ? "accent-bg accent-fg" : "sunken text-muted hover:text-app"}`}>
                  {vr.sku}{vr.color ? ` · ${vr.color}` : ""}
                </button>
              ))}
            </div>
            {view === "table" ? (
              <div style={{ maxHeight: 520, overflow: "auto" }}>
                <table className="axlu-table">
                  <tbody>
                    {flattenObject(variantRaw).map(([k, val]) => (
                      <tr key={k}>
                        <td className="mono text-[11.5px] text-muted w-[260px]" style={{ verticalAlign: "top" }}>{k}</td>
                        <td className="mono text-[11.5px]" style={{ wordBreak: "break-word" }}>{val}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            ) : (
              <pre className="p-4 mono text-[11.5px] overflow-auto leading-relaxed" style={{ maxHeight: 520 }}>{JSON.stringify(variantRaw, null, 2)}</pre>
            )}
          </>
        )}
      </Card>
    </div>
  );
}

// Flatten a nested object into [path, value] rows for the raw-data table.
function flattenObject(obj, prefix = "") {
  const rows = [];
  if (obj == null || typeof obj !== "object") return [[prefix || "(valeur)", String(obj)]];
  for (const [k, v] of Object.entries(obj)) {
    const path = prefix ? `${prefix}.${k}` : k;
    if (v && typeof v === "object" && !Array.isArray(v)) {
      const sub = flattenObject(v, path);
      if (sub.length === 0) rows.push([path, "{}"]);
      else rows.push(...sub);
    } else if (Array.isArray(v)) {
      if (v.length === 0) rows.push([path, "[]"]);
      else if (v.every((x) => x == null || typeof x !== "object")) rows.push([path, v.join(", ")]);
      else rows.push([path, JSON.stringify(v)]);
    } else {
      rows.push([path, v == null || v === "" ? "—" : String(v)]);
    }
  }
  return rows;
}

// ─── AXLU data tab (with diff panel) ───────────────────────────────────
function ProductDataTab({ product, edited, setEdited }) {
  const v = useAxluStore();
  const { CATEGORIES_AXLU } = window.AxluData;
  const toast = useToast();
  const [catSearch, setCatSearch] = useState('');
  const [catPickerOpen, setCatPickerOpen] = useState(false);

  const baseline = useMemo(() => ({
    title: product.title || '',
    description: product.description || '',
    categoryIds: Array.isArray(product.categoryIds) && product.categoryIds.length
      ? product.categoryIds : (product.categoryId ? [product.categoryId] : []),
    brand: product.brand || '',
    material: product.material || '',
    color: product.primaryColor || "Multi",
    weight: product.weight || 0,
    dimL: product.dimensions.l, dimW: product.dimensions.w,
    dimH: product.dimensions.h, dimD: product.dimensions.d || 0,
  }), [product.id]);

  const isDirty = JSON.stringify(baseline) !== JSON.stringify(edited);
  const set = (patch) => setEdited({ ...edited, ...patch });
  const catIds = edited.categoryIds || [];

  const handleSave = () => {
    window.AxluStore.dispatch('product.update', {
      id: product.id,
      manual: true,
      patch: {
        title: edited.title,
        description: edited.description,
        categoryIds: catIds,
        categoryId: catIds[0] || null,
        categoryLabel: (CATEGORIES_AXLU.find((c) => c.id === catIds[0]) || {}).name || '—',
        brand: edited.brand,
        material: edited.material,
        primaryColor: edited.color,
        weight: parseFloat(edited.weight) || 0,
        dimensions: {
          l: parseFloat(edited.dimL) || 0,
          w: parseFloat(edited.dimW) || 0,
          h: parseFloat(edited.dimH) || 0,
          d: parseFloat(edited.dimD) || 0,
        },
      },
    });
    toast('Modifications enregistrées — protégées des ré-imports', { tone: 'success' });
  };
  const handleCancel = () => setEdited(baseline);

  // Categories available to add (not already assigned), with parent context
  // so the searchable picker can show "Groupe › Sous-catégorie".
  const catOptions = CATEGORIES_AXLU
    .filter((c) => !catIds.includes(c.id))
    .map((c) => {
      const parent = c.parent ? CATEGORIES_AXLU.find((x) => x.id === c.parent) : null;
      return {
        id: c.id, name: c.name,
        parentName: parent ? parent.name : null,
        search: ((parent ? parent.name + ' ' : '') + c.name).toLowerCase(),
      };
    });
  const catQuery = catSearch.trim().toLowerCase();
  // The picker proposes every category; typing in the search box filters it.
  const catList = catQuery ? catOptions.filter((o) => o.search.includes(catQuery)) : catOptions;

  const addCategory = (id) => { if (id && !catIds.includes(id)) set({ categoryIds: [...catIds, id] }); };
  const removeCategory = (id) => set({ categoryIds: catIds.filter((x) => x !== id) });

  const hasDiameter = (product.dimensions && product.dimensions.d > 0) || (parseFloat(edited.dimD) || 0) > 0;

  return (
    <div className="space-y-4">
      <Card title="Identité produit">
        <div className="space-y-4">
          <Field label="Titre">
            <Input value={edited.title} onChange={(e) => set({ title: e.target.value })} size="lg"/>
          </Field>
          <Field label="Description">
            <textarea value={edited.description}
              onChange={(e) => set({ description: e.target.value })}
              className="w-full surface border rounded-md p-3 text-[13px] ring-accent leading-relaxed" rows={12}
              style={{ borderColor: "var(--border)", resize: "vertical", minHeight: 220 }}/>
          </Field>
          <Field label="Marque">
            <Input value={edited.brand} onChange={(e) => set({ brand: e.target.value })} className="max-w-[360px]"/>
          </Field>
        </div>
      </Card>

      <Card title="Catégories AXLU">
        <div className="text-[12px] text-muted mb-2">
          Un produit peut être rangé dans plusieurs catégories / sous-catégories AXLU.
        </div>
        {catIds.length > 0 ? (
          <div className="flex flex-wrap gap-1.5 mb-3">
            {catIds.map((id) => {
              const cat = CATEGORIES_AXLU.find((c) => c.id === id);
              const parent = cat && cat.parent ? CATEGORIES_AXLU.find((c) => c.id === cat.parent) : null;
              return (
                <span key={id} className="inline-flex items-center gap-1 px-2 py-1 sunken rounded text-[12px]">
                  <Icon name="tag" size={11} className="text-subtle shrink-0"/>
                  {parent && <><span className="text-muted">{parent.name}</span><span className="text-subtle">›</span></>}
                  <span className="font-medium">{cat ? cat.name : id}</span>
                  <button type="button" onClick={() => removeCategory(id)} className="text-subtle hover:text-app ml-0.5">
                    <Icon name="x" size={10}/>
                  </button>
                </span>
              );
            })}
          </div>
        ) : (
          <div className="text-[12px] text-amber-600 mb-3">Aucune catégorie assignée — produit non rangé.</div>
        )}
        <div>
          <span className="text-[12px] text-muted">Ajouter une catégorie / sous-catégorie :</span>
          <div className="mt-1.5 relative max-w-[380px]">
            <Input leftIcon="search" value={catSearch}
                   onChange={(e) => { setCatSearch(e.target.value); setCatPickerOpen(true); }}
                   onFocus={() => setCatPickerOpen(true)}
                   placeholder="Choisir ou rechercher une catégorie…"/>
            {catPickerOpen && (
              <>
                <div className="fixed inset-0 z-10" onClick={() => setCatPickerOpen(false)}/>
                <div className="absolute z-20 mt-1 w-full max-h-[280px] overflow-y-auto surface border rounded-md"
                     style={{ borderColor: "var(--border)", boxShadow: "var(--shadow-lg)" }}>
                  {catList.length === 0 ? (
                    <div className="px-2.5 py-2 text-[12px] text-subtle">
                      {catOptions.length === 0
                        ? "Toutes les catégories sont déjà assignées à ce produit."
                        : "Aucune catégorie ne correspond à la recherche."}
                    </div>
                  ) : catList.map((o) => (
                    <button key={o.id} type="button"
                            onClick={() => { addCategory(o.id); setCatSearch(''); }}
                            className="w-full text-left px-2.5 py-1.5 text-[12px] hover-row flex items-center gap-1">
                      {o.parentName && <><span className="text-muted">{o.parentName}</span><span className="text-subtle">›</span></>}
                      <span className="font-medium">{o.name}</span>
                    </button>
                  ))}
                </div>
              </>
            )}
          </div>
        </div>
        <div className="text-[11px] text-subtle mt-2">
          Les catégories se créent dans l'écran <a href="#/categories" className="accent-fg hover:underline">Catégories</a>.
        </div>
      </Card>

      <Card title="Caractéristiques techniques">
        <div className="grid grid-cols-2 gap-4">
          <Field label="Matière">
            <Input value={edited.material} onChange={(e) => set({ material: e.target.value })}/>
          </Field>
          <Field label="Couleur principale">
            <Input value={edited.color} onChange={(e) => set({ color: e.target.value })}/>
          </Field>
          <Field label="Poids (g)">
            <Input value={edited.weight} onChange={(e) => set({ weight: e.target.value })} className="mono"/>
          </Field>
          <Field label="Diamètre (cm)">
            <Input value={edited.dimD} onChange={(e) => set({ dimD: e.target.value })} className="mono"
                   placeholder={hasDiameter ? "" : "non applicable"}/>
          </Field>
        </div>
        <div className="mt-4">
          <Field label="Dimensions L × l × H (cm)">
            <div className="flex items-center gap-2 max-w-[360px]">
              <Input value={edited.dimL} onChange={(e) => set({ dimL: e.target.value })} className="mono"/>
              <span className="text-subtle">×</span>
              <Input value={edited.dimW} onChange={(e) => set({ dimW: e.target.value })} className="mono"/>
              <span className="text-subtle">×</span>
              <Input value={edited.dimH} onChange={(e) => set({ dimH: e.target.value })} className="mono"/>
            </div>
          </Field>
        </div>
      </Card>

      <div className="flex justify-end gap-2">
        <Button variant="ghost" onClick={handleCancel} disabled={!isDirty}>Annuler</Button>
        <Button variant="primary" leftIcon="check" onClick={handleSave} disabled={!isDirty}>Enregistrer</Button>
      </div>
    </div>
  );
}

// ─── Variants tab ──────────────────────────────────────────────────────
function ProductVariantsTab({ product }) {
  const v = useAxluStore();
  const toast = useToast();
  const [open, setOpen] = useState(false);
  const [draft, setDraft] = useState({ color: '', size: '', sku: '', ean: '', stock: 0, buyPrice: 0 });

  // Product sell price for one unit, excluding marking (buy price × base coef).
  const costs = window.AxluData.PRICING_COSTS || {};
  const baseCoef = costs.baseCoef || 2.5;
  const sellPerUnit = (buy) => {
    const b = Number(buy) || 0;
    return b > 0 ? b * baseCoef : 0;
  };

  const handleAdd = () => {
    if (!draft.color || !draft.sku) {
      toast('Couleur et SKU requis', { tone: 'warning' });
      return;
    }
    const newVariant = {
      sku: draft.sku, color: draft.color,
      ...(draft.size ? { size: draft.size } : {}),
      ean: draft.ean || '',
      stock: parseInt(draft.stock) || 0,
      buyPrice: parseFloat(draft.buyPrice) || 0,
      priceTiers: [], printMethods: [],
    };
    window.AxluStore.dispatch('product.update', {
      id: product.id, patch: { variants: [...product.variants, newVariant] },
    });
    toast('Variante ajoutée', { tone: 'success' });
    setOpen(false);
    setDraft({ color: '', size: '', sku: '', ean: '', stock: 0, buyPrice: 0 });
  };
  const handleDelete = (idx) => {
    window.AxluStore.dispatch('product.update', {
      id: product.id, patch: { variants: product.variants.filter((_, i) => i !== idx) },
    });
    toast('Variante supprimée', { tone: 'success' });
  };

  const hasSizes = product.variants.some((vr) => vr.size);

  return (
    <Card padding="none" title={`${product.variants.length} variante${product.variants.length > 1 ? "s" : ""}`}
          action={<Button size="sm" variant="secondary" leftIcon="plus" onClick={() => setOpen(true)}>Ajouter une variante</Button>}>
      <table className="axlu-table">
        <thead>
          <tr>
            <th>SKU variante</th>
            <th>Couleur</th>
            {hasSizes && <th>Taille</th>}
            <th>Code EAN</th>
            <th>Prix de vente (1 pièce, hors marquage)</th>
            <th>Stock disponible</th>
            <th>Disponibilité</th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          {product.variants.map((vr, i) => (
            <tr key={vr.sku || i} className="hover-row">
              <td className="mono text-[12px]">{vr.sku}</td>
              <td>
                <div className="flex items-center gap-2">
                  <ColorDot hex={vr.colorHex} name={vr.color}/>
                  <span className="text-[12.5px]">{vr.color || "—"}</span>
                </div>
              </td>
              {hasSizes && <td className="mono text-[12px]">{vr.size || "—"}</td>}
              <td className="mono text-[12px] text-muted">{vr.ean || "—"}</td>
              <td className="mono tabular-nums">
                {vr.buyPrice > 0 ? fmt.eur(sellPerUnit(vr.buyPrice)) : <span className="text-subtle text-[11px]">prix non importé</span>}
              </td>
              <td className={`mono tabular-nums ${vr.stockUncounted ? "text-emerald-600" : vr.stock === 0 ? "text-red-600" : vr.stock < 50 ? "text-amber-600" : ""}`}>
                {stockText(vr.stock, vr.stockUncounted)}
              </td>
              <td>{stockBadge(vr.stock || 0, vr.stockUncounted)}</td>
              <td className="text-right">
                <IconButton icon="trash" size="sm" onClick={() => handleDelete(i)}/>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      {open && (
        <Modal open={open} onClose={() => setOpen(false)} title="Ajouter une variante"
               footer={<>
                 <Button variant="ghost" onClick={() => setOpen(false)}>Annuler</Button>
                 <Button variant="primary" leftIcon="check" onClick={handleAdd}>Ajouter</Button>
               </>}>
          <div className="space-y-3">
            <Field label="Couleur"><Input value={draft.color} onChange={(e) => setDraft({ ...draft, color: e.target.value })} placeholder="Ex: Noir"/></Field>
            <Field label="Taille (optionnel)"><Input value={draft.size} onChange={(e) => setDraft({ ...draft, size: e.target.value })} placeholder="Ex: M"/></Field>
            <div className="grid grid-cols-2 gap-3">
              <Field label="SKU"><Input value={draft.sku} onChange={(e) => setDraft({ ...draft, sku: e.target.value })} className="mono"/></Field>
              <Field label="Code EAN"><Input value={draft.ean} onChange={(e) => setDraft({ ...draft, ean: e.target.value })} className="mono"/></Field>
            </div>
            <div className="grid grid-cols-2 gap-3">
              <Field label="Stock"><Input value={draft.stock} onChange={(e) => setDraft({ ...draft, stock: e.target.value })} className="mono"/></Field>
              <Field label="Prix d'achat (€)"><Input value={draft.buyPrice} onChange={(e) => setDraft({ ...draft, buyPrice: e.target.value })} className="mono"/></Field>
            </div>
          </div>
        </Modal>
      )}
    </Card>
  );
}

// Colour preview dot — uses the real PF hex when available.
function ColorDot({ hex, name, size = 13 }) {
  const clean = (hex || "").toString().trim().replace(/^#/, "");
  const valid = /^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{6}$/.test(clean);
  return (
    <span className="rounded-full border shrink-0" title={name || ""}
          style={{ width: size, height: size, display: "inline-block",
                   background: valid ? `#${clean}` : "var(--surface-2)",
                   borderColor: "var(--border-strong)" }}/>
  );
}

// Stock availability badge — based purely on the direct stock quantity.
function stockBadge(s, uncounted) {
  if (uncounted) return <Badge tone="success" dot>Disponible</Badge>;
  if (s === 0) return <Badge tone="danger" dot>Indisponible</Badge>;
  if (s < 50) return <Badge tone="warning" dot>Stock faible</Badge>;
  return <Badge tone="success" dot>Disponible</Badge>;
}

// Stock value as text — "Disponible" when PF gave no real count (sentinel),
// otherwise the real number.
function stockText(stock, uncounted) {
  if (uncounted) return "Disponible";
  return fmt.num(Number(stock) || 0);
}

// ─── Images tab ────────────────────────────────────────────────────────
// Shows the supplier (PF) images: the model gallery + a thumbnail per colour
// variant. Custom uploaded images are still supported in a separate section.
function ProductImagesTab({ product }) {
  const v = useAxluStore();
  const toast = useToast();
  const fileInputRef = useRef(null);
  const [lightbox, setLightbox] = useState(null);
  const [confirmDelete, setConfirmDelete] = useState(null);

  const big = (url) => (url || "").replace("/500x500/", "/1600x1600/");

  // Full gallery per colour variant: main image + every available view.
  const variantGalleries = (product.variants || []).map((vr) => {
    const imgs = [];
    if (vr.imageUrl) imgs.push(vr.imageUrl);
    (vr.imageGallery || []).forEach((u) => { if (u && !imgs.includes(u)) imgs.push(u); });
    return { sku: vr.sku, color: vr.color || vr.sku, colorHex: vr.colorHex, images: imgs };
  }).filter((g) => g.images.length > 0);

  const totalImages = variantGalleries.reduce((s, g) => s + g.images.length, 0);
  const uploaded = product.images || [];

  const handleFiles = (files) => {
    if (!files || !files.length) return;
    const readers = Array.from(files).map((f) => new Promise((res) => {
      const r = new FileReader();
      r.onload = () => res({ name: f.name, src: r.result });
      r.readAsDataURL(f);
    }));
    Promise.all(readers).then((newImgs) => {
      window.AxluStore.dispatch('product.update', {
        id: product.id, patch: { images: [...uploaded, ...newImgs] },
      });
      toast(`${newImgs.length} image(s) ajoutée(s)`, { tone: 'success' });
    });
  };
  const handleDelete = (idx) => {
    window.AxluStore.dispatch('product.update', {
      id: product.id, patch: { images: uploaded.filter((_, i) => i !== idx) },
    });
    toast('Image supprimée', { tone: 'success' });
    setConfirmDelete(null);
  };

  const ImgTile = ({ url, label, badge, onClick }) => (
    <div className="relative group cursor-pointer" onClick={onClick}>
      <div className="aspect-square rounded-md overflow-hidden flex items-center justify-center" style={{ background: "#fff", boxShadow: "inset 0 0 0 1px var(--border)" }}>
        <img src={url} alt={label || ""} loading="lazy" draggable={false}
             style={{ maxWidth: "100%", maxHeight: "100%", objectFit: "contain" }}
             onError={(e) => { e.currentTarget.parentElement.classList.add("img-ph"); e.currentTarget.style.display = "none"; }}/>
      </div>
      {badge && <div className="absolute top-2 left-2"><Badge tone="info">{badge}</Badge></div>}
      {label && <div className="mt-1.5 text-[11px] text-subtle truncate">{label}</div>}
    </div>
  );

  return (
    <div className="space-y-6">
      <Card title="Images par variante" padding="md"
            action={<Badge tone={totalImages ? "info" : "muted"}>{totalImages} image{totalImages > 1 ? "s" : ""} · {variantGalleries.length} variante{variantGalleries.length > 1 ? "s" : ""}</Badge>}>
        {variantGalleries.length === 0 ? (
          <EmptyState icon="image" title="Aucune image fournisseur"
                      description="Les images proviennent du flux Produits PF. Réimportez le flux Produits si elles manquent."/>
        ) : (
          <div className="space-y-4">
            {variantGalleries.map((g, gi) => (
              <div key={g.sku || gi} className={gi > 0 ? "pt-4 border-t" : ""} style={{ borderColor: "var(--border)" }}>
                <div className="flex items-center gap-2 mb-2">
                  {g.colorHex && (
                    <span className="w-3.5 h-3.5 rounded-full border" style={{ background: "#" + String(g.colorHex).replace(/^#/, ""), borderColor: "var(--border)" }}/>
                  )}
                  <span className="text-[13px] font-medium">{g.color}</span>
                  <span className="text-[11px] text-subtle mono">{g.sku}</span>
                  <span className="text-[11px] text-subtle">· {g.images.length} vue{g.images.length > 1 ? "s" : ""}</span>
                </div>
                <div className="grid grid-cols-8 gap-2">
                  {g.images.map((url, i) => (
                    <ImgTile key={i} url={url} label={i === 0 ? "Principale" : `Vue ${i + 1}`}
                             onClick={() => setLightbox({ src: big(url), name: `${g.color} · vue ${i + 1}` })}/>
                  ))}
                </div>
              </div>
            ))}
          </div>
        )}
      </Card>

      <Card title="Images ajoutées manuellement" padding="md"
            action={<>
              <input ref={fileInputRef} type="file" accept="image/*" multiple style={{ display: 'none' }}
                     onChange={(e) => handleFiles(e.target.files)}/>
              <Button size="sm" variant="secondary" leftIcon="upload" onClick={() => fileInputRef.current?.click()}>Ajouter</Button>
            </>}>
        <div className="grid grid-cols-5 gap-3"
             onDragOver={(e) => e.preventDefault()}
             onDrop={(e) => { e.preventDefault(); handleFiles(e.dataTransfer.files); }}>
          {uploaded.map((img, i) => (
            <div key={i} className="relative group">
              <div className="aspect-square rounded-md bg-cover bg-center" style={{ backgroundImage: `url(${img.src})`, boxShadow: "inset 0 0 0 1px var(--border)" }}/>
              <div className="absolute inset-0 bg-black/40 rounded-md opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-1">
                <IconButton icon="eye" size="sm" className="!bg-white/20 !text-white" onClick={() => setLightbox(img)}/>
                <IconButton icon="trash" size="sm" className="!bg-white/20 !text-white" onClick={() => setConfirmDelete(i)}/>
              </div>
              <div className="mt-1.5 text-[11px] text-subtle mono truncate">{img.name || `img-${i + 1}.jpg`}</div>
            </div>
          ))}
          <button onClick={() => fileInputRef.current?.click()}
                  onDragOver={(e) => e.preventDefault()}
                  onDrop={(e) => { e.preventDefault(); handleFiles(e.dataTransfer.files); }}
                  className="aspect-square border-2 border-dashed rounded-md flex flex-col items-center justify-center gap-1 text-muted hover:text-app transition-colors" style={{ borderColor: "var(--border-strong)" }}>
            <Icon name="upload" size={20}/>
            <span className="text-[11px]">Glisser-déposer</span>
          </button>
        </div>
      </Card>

      {lightbox && (
        <Modal open={!!lightbox} onClose={() => setLightbox(null)} title={lightbox.name || "Aperçu image"} width={760}
               footer={<Button variant="secondary" onClick={() => setLightbox(null)}>Fermer</Button>}>
          <div className="flex items-center justify-center" style={{ background: "#fff", borderRadius: 6 }}>
            <img src={lightbox.src} alt={lightbox.name || ''} style={{ maxWidth: '100%', maxHeight: '64vh', objectFit: "contain" }}/>
          </div>
        </Modal>
      )}

      {confirmDelete != null && (
        <Modal open={confirmDelete != null} onClose={() => setConfirmDelete(null)} title="Supprimer l'image"
               footer={<>
                 <Button variant="ghost" onClick={() => setConfirmDelete(null)}>Annuler</Button>
                 <Button variant="danger" leftIcon="trash" onClick={() => handleDelete(confirmDelete)}>Supprimer</Button>
               </>}>
          <div className="text-[13px]">Supprimer cette image ajoutée manuellement ?</div>
        </Modal>
      )}
    </div>
  );
}

// Is a price-combination's colour count within what the product's method allows?
function colorComboAllowed(comboColors, methodMax) {
  const cFull = /full/i.test(String(comboColors || ""));
  const mFull = /full/i.test(String(methodMax || ""));
  if (cFull) return mFull;
  const cNum = parseInt(comboColors, 10);
  const mNum = parseInt(methodMax, 10);
  if (Number.isFinite(cNum) && Number.isFinite(mNum)) return cNum <= mNum;
  return true;
}

// Physical imprint area of a marking zone, in cm². Prefers the feed-provided
// maxAreaCm2; otherwise derives it from the imprint dimensions.
function zoneAreaCm2(z) {
  if (!z) return 0;
  if (z.maxAreaCm2 > 0) return z.maxAreaCm2;
  if (z.impDiameterMm > 0) {
    const r = z.impDiameterMm / 20; // mm → cm radius
    return Math.PI * r * r;
  }
  if (z.impWidthMm > 0 && z.impHeightMm > 0) {
    return (z.impWidthMm / 10) * (z.impHeightMm / 10);
  }
  return 0;
}

// Largest imprint area available for a technique (max across its zones).
function methodMaxAreaCm2(zones) {
  let max = 0;
  (zones || []).forEach((z) => { const a = zoneAreaCm2(z); if (a > max) max = a; });
  return max;
}

// Restrict a generic PF print-price matrix to what THIS product can actually
// order: (1) colour count within the technique's maxColors, (2) logo-size
// bracket that fits the product's largest imprint zone. A size bracket B is
// kept only when its lower edge (the previous bracket) is below the product's
// max imprint area — so a 300 cm² zone keeps the 100 & 300 cm² brackets but
// drops 500 / 700. Each filter degrades gracefully when its data is missing.
function productPriceMatrix(priceEntry, methodMaxColors, zones) {
  const all = (priceEntry && priceEntry.priceMatrix) || [];
  if (!all.length) return [];
  // (1) Colour-count filter.
  let m = all.filter((c) => colorComboAllowed(c.colors, methodMaxColors));
  if (!m.length) m = all.slice();
  // (2) Logo-size bracket filter — only when the imprint area is known.
  const maxArea = methodMaxAreaCm2(zones);
  if (maxArea > 0) {
    const sizes = [...new Set(m.map((c) => c.logoSizeCm2 || 0))].sort((a, b) => a - b);
    const allowedSizes = new Set();
    sizes.forEach((s, i) => {
      const lowerEdge = i === 0 ? 0 : sizes[i - 1];
      if (lowerEdge < maxArea - 0.5) allowedSizes.add(s);
    });
    const sized = m.filter((c) => allowedSizes.has(c.logoSizeCm2 || 0));
    if (sized.length) m = sized;
  }
  return m;
}

// Group a product's per-variant print methods by printCode. Zones are
// deduplicated by physical identity (location + dimensions), since the PF
// `ref` differs per colour variant for the same physical zone.
function buildMarkingGroups(product) {
  const zoneSig = (m) => `${(m.location || "").toLowerCase()}|${m.impWidthMm || 0}x${m.impHeightMm || 0}x${m.impDiameterMm || 0}`;
  const groups = new Map();
  ((product && product.variants) || []).forEach((vr) => {
    (vr.printMethods || []).forEach((m) => {
      if (!m.printCode) return;
      if (!groups.has(m.printCode)) {
        groups.set(m.printCode, {
          printCode: m.printCode, method: m.method,
          maxColors: m.maxColors, isDefault: !!m.isDefault, zones: [],
        });
      }
      const g = groups.get(m.printCode);
      if (m.isDefault) g.isDefault = true;
      const sig = zoneSig(m);
      if (!g.zones.some((z) => z.sig === sig)) {
        g.zones.push({
          sig, ref: m.ref, location: m.location,
          impWidthMm: m.impWidthMm, impHeightMm: m.impHeightMm, impDiameterMm: m.impDiameterMm,
          maxAreaCm2: m.maxAreaCm2, leadTime: m.leadTime, image: m.image,
        });
      }
    });
  });
  return [...groups.values()];
}

// ─── Marking tab ───────────────────────────────────────────────────────
function ProductMarkingTab({ product }) {
  const v = useAxluStore();
  const { PF_PRINT_PRICES, PRICING_COSTS, MARKING_COEFS } = window.AxluData;
  const [expanded, setExpanded] = useState(null);
  const globalCoef = (PRICING_COSTS && PRICING_COSTS.markingCoef) || 2;
  // Effective coefficient for a technique: per-technique override or global.
  const coefFor = (printCode) => {
    const o = MARKING_COEFS && MARKING_COEFS[printCode];
    return (o != null) ? o : globalCoef;
  };

  // Group print methods by printCode (zones deduplicated by physical identity).
  const methods = buildMarkingGroups(product);
  const hasMethods = methods.length > 0;

  const priceByCode = new Map();
  (PF_PRINT_PRICES || []).forEach((p) => { if (p.printCode) priceByCode.set(p.printCode, p); });
  const printPricesCount = (PF_PRINT_PRICES || []).length;
  const hasPrices = printPricesCount > 0;

  // Setup achat (PF cost) + setup vente (override if saved, else cost × coef).
  // Filtered to what the product can actually order (colours + imprint area).
  // Returns null if no barème.
  const setupInfo = (m, pe) => {
    if (!pe || !pe.priceMatrix || !pe.priceMatrix.length) return null;
    const mx = productPriceMatrix(pe, m.maxColors, m.zones);
    const override = (product.markingPriceOverrides || {})[m.printCode];
    const coef = coefFor(m.printCode);
    const buys = [], sells = [];
    mx.forEach((c) => {
      if (c.setupCharge > 0) buys.push(c.setupCharge);
      const ov = override && override[comboKey(c)];
      const sell = (ov && ov.setupSell != null) ? ov.setupSell : (c.setupCharge || 0) * coef;
      if (sell > 0) sells.push(sell);
    });
    return {
      buyMin: buys.length ? Math.min(...buys) : 0,
      buyMax: buys.length ? Math.max(...buys) : 0,
      sellMin: sells.length ? Math.min(...sells) : 0,
      sellMax: sells.length ? Math.max(...sells) : 0,
    };
  };
  const range = (a, b) => (a === b ? fmt.eur(a) : `${fmt.eur(a)} – ${fmt.eur(b)}`);

  if (!hasMethods) {
    return (
      <Card padding="lg">
        <EmptyState icon="tag" title="Aucune méthode de marquage importée"
                    description="Lancez le flux « Données d'impression » du fournisseur (et importez-le en entier) pour récupérer les techniques disponibles pour ce produit."/>
      </Card>
    );
  }

  return (
    <div className="space-y-4">
      {!hasPrices && (
        <div className="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>Aucun barème de prix d'impression importé. Lancez le flux <strong>Prix d'impression</strong> avec <strong>« Importer tout »</strong>.</div>
        </div>
      )}
      {hasPrices && printPricesCount < 50 && (
        <div className="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>Seulement <strong>{printPricesCount} codes</strong> de prix d'impression importés (~135 attendus). Relancez le flux <strong>Prix d'impression</strong> avec <strong>« Importer tout »</strong>.</div>
        </div>
      )}

      <Card title="Méthodes de marquage disponibles" padding="none"
            action={<Badge tone="info">{methods.length} technique{methods.length > 1 ? "s" : ""}</Badge>}>
        <div className="px-4 py-2 text-[12px] text-muted border-b" style={{ borderColor: "var(--border)" }}>
          Cliquez une technique pour voir ses zones et ajuster ses prix de vente. Prix vente = coût PF × coefficient
          (× {globalCoef} global, surchargeable par technique dans <a href="#/pricing" className="accent-fg hover:underline">Pricing</a>).
        </div>
        <table className="axlu-table">
          <thead>
            <tr>
              <th style={{ width: 28 }}></th>
              <th>Code</th>
              <th>Technique</th>
              <th>Zones</th>
              <th>Couleurs max</th>
              <th>Setup achat</th>
              <th>Setup vente</th>
            </tr>
          </thead>
          <tbody>
            {methods.map((m) => {
              const pe = priceByCode.get(m.printCode);
              const si = setupInfo(m, pe);
              const override = (product.markingPriceOverrides || {})[m.printCode];
              const isOpen = expanded === m.printCode;
              return (
                <Fragment key={m.printCode}>
                  <tr className="hover-row cursor-pointer" onClick={() => setExpanded(isOpen ? null : m.printCode)}>
                    <td><Icon name={isOpen ? "chevronDown" : "chevronRight"} size={13} className="text-subtle"/></td>
                    <td className="mono text-[12px] font-medium">
                      {m.printCode}{m.isDefault && <span className="ml-1 text-amber-500" title="Technique par défaut">★</span>}
                    </td>
                    <td className="font-medium">{m.method || "—"}</td>
                    <td className="mono text-[12px]">{m.zones.length}</td>
                    <td className="mono text-[12px]">{m.maxColors || "—"}</td>
                    <td className="mono tabular-nums text-muted text-[12px]">
                      {si == null ? "—" : range(si.buyMin, si.buyMax)}
                    </td>
                    <td className="mono tabular-nums text-[12px]">
                      {si == null
                        ? <span className="text-subtle text-[11px]">{hasPrices ? "code absent" : "non importé"}</span>
                        : <span className="font-medium">{range(si.sellMin, si.sellMax)}{override && <span className="ml-1 text-amber-500" title="Prix forcés manuellement">●</span>}</span>}
                    </td>
                  </tr>
                  {isOpen && (
                    <tr>
                      <td colSpan={7} style={{ padding: 0 }}>
                        <MarkingMethodDetail group={m} priceEntry={pe} effCoef={coefFor(m.printCode)} product={product}/>
                      </td>
                    </tr>
                  )}
                </Fragment>
              );
            })}
          </tbody>
        </table>
      </Card>
    </div>
  );
}

function zoneDims(z) {
  if (z.impDiameterMm) return `Ø ${z.impDiameterMm} mm`;
  if (z.impWidthMm || z.impHeightMm) return `${z.impWidthMm} × ${z.impHeightMm} mm`;
  return "—";
}

function comboKey(c) { return `${c.colors || ""}|${c.logoSizeCm2 || 0}`; }
function round2(n) { return Math.round((Number(n) || 0) * 100) / 100; }

// Expandable detail for a marking technique: zones + editable sell prices.
function MarkingMethodDetail({ group, priceEntry, effCoef, product }) {
  const m = group;
  const pe = priceEntry;
  const toast = useToast();
  const [openCombo, setOpenCombo] = useState(0);

  const allMatrix = (pe && pe.priceMatrix) || [];
  const shownMatrix = productPriceMatrix(pe, m.maxColors, m.zones);
  const filteredOut = allMatrix.length - shownMatrix.length;
  const maxArea = methodMaxAreaCm2(m.zones);

  // Manual sell-price override for this product + technique (if any).
  const override = (product.markingPriceOverrides || {})[m.printCode] || null;

  // Editable sell prices. Pre-filled from override (if saved) else computed
  // (PF cost × effCoef). Keyed by combo index → { setup, tiers: {tierIdx} }.
  const buildEdit = () => {
    const st = {};
    shownMatrix.forEach((c, ci) => {
      const ov = override && override[comboKey(c)];
      st[ci] = {
        setup: String(ov && ov.setupSell != null ? ov.setupSell : round2((c.setupCharge || 0) * effCoef)),
        tiers: {},
      };
      (c.tiers || []).forEach((t, ti) => {
        const ovTier = ov && ov.tiers ? ov.tiers[t.fromQty] : null;
        st[ci].tiers[ti] = String(ovTier != null ? ovTier : round2((t.price || 0) * effCoef));
      });
    });
    return st;
  };
  const [edit, setEdit] = useState(buildEdit);
  const [dirty, setDirty] = useState(false);

  const setCell = (ci, field, ti, val) => {
    setDirty(true);
    setEdit((prev) => {
      const next = { ...prev, [ci]: { ...prev[ci], tiers: { ...prev[ci].tiers } } };
      if (field === "setup") next[ci].setup = val;
      else next[ci].tiers[ti] = val;
      return next;
    });
  };

  const save = () => {
    const out = {};
    shownMatrix.forEach((c, ci) => {
      const e = edit[ci] || { setup: "0", tiers: {} };
      const tiers = {};
      (c.tiers || []).forEach((t, ti) => { tiers[t.fromQty] = parseFloat(e.tiers[ti]) || 0; });
      out[comboKey(c)] = { setupSell: parseFloat(e.setup) || 0, tiers };
    });
    window.AxluStore.dispatch("product.setMarkingPrices", { id: product.id, printCode: m.printCode, matrix: out });
    setDirty(false);
    toast(`Prix de marquage ${m.printCode} enregistrés`, { tone: "success" });
  };

  const resetToComputed = () => {
    window.AxluStore.dispatch("product.setMarkingPrices", { id: product.id, printCode: m.printCode, matrix: null });
    setDirty(false);
    toast(`Prix ${m.printCode} réinitialisés au calcul automatique`);
  };

  return (
    <div className="p-4 sunken space-y-4" style={{ borderTop: "1px solid var(--border)" }}>
      {/* Zones */}
      <div className="surface border rounded-md p-3" style={{ borderColor: "var(--border)" }}>
        <div className="text-[12px] font-semibold mb-2">
          Zones de marquage <span className="text-muted font-normal">({m.zones.length})</span>
        </div>
        <div className="grid grid-cols-3 gap-3">
          {m.zones.map((z, i) => (
            <div key={z.ref || i} className="border rounded-md p-2.5" style={{ borderColor: "var(--border)" }}>
              <div className="font-medium text-[12.5px] mb-1">{z.location || `Zone ${i + 1}`}</div>
              <dl className="space-y-0.5 text-[11.5px]">
                <div className="flex justify-between"><dt className="text-muted">Dimensions</dt><dd className="mono">{zoneDims(z)}</dd></div>
                <div className="flex justify-between"><dt className="text-muted">Surface</dt><dd className="mono">{z.maxAreaCm2 ? `${z.maxAreaCm2} cm²` : "—"}</dd></div>
                <div className="flex justify-between"><dt className="text-muted">Délai</dt><dd className="mono">{z.leadTime ? `${z.leadTime} j` : "—"}</dd></div>
              </dl>
              {z.image && (
                <img src={`https://images.pfconcept.com/ImprintImages_All/JPG/500x500/${z.image}`}
                     alt="" style={{ width: "100%", maxHeight: 120, objectFit: "contain", marginTop: 6, border: "1px solid var(--border)", borderRadius: 4, background: "#fff" }}
                     onError={(e) => { e.currentTarget.style.display = "none"; }}/>
              )}
            </div>
          ))}
        </div>
      </div>

      {/* Pricing — editable sell prices. */}
      <div className="surface border rounded-md p-3" style={{ borderColor: "var(--border)" }}>
        <div className="flex items-center gap-2 mb-2 flex-wrap">
          <div className="text-[12px] font-semibold">Prix de marquage</div>
          <Badge tone="info">coefficient × {effCoef}</Badge>
          {override && <Badge tone="warning">Prix forcés manuellement</Badge>}
          <div className="flex-1"/>
          {override && (
            <Button size="sm" variant="ghost" onClick={resetToComputed}>Réinitialiser au calcul</Button>
          )}
          <Button size="sm" variant="primary" leftIcon="check" disabled={!dirty} onClick={save}>
            Enregistrer les prix
          </Button>
        </div>
        {!pe || !allMatrix.length ? (
          <div className="text-[12px] text-muted">
            Aucun tarif pour <span className="mono">{m.printCode}</span> dans le barème importé.
            Importez le flux <strong>Prix d'impression</strong> en entier.
          </div>
        ) : (
          <div className="space-y-3">
            {pe.priceDependence && pe.priceDependence !== "None" && (
              <Badge tone="neutral">Le prix dépend de : {pe.priceDependence}</Badge>
            )}
            {pe.ltmCharge > 0 && (
              <div className="text-[11.5px] text-muted">Frais sous-quantité (LTM) : coût {fmt.eur(pe.ltmCharge)} · vente {fmt.eur(pe.ltmCharge * effCoef)}</div>
            )}
            {filteredOut > 0 && (
              <div className="text-[11px] text-muted">
                Barème restreint aux capacités réelles du produit : <strong>{m.maxColors || "?"} couleur{/^1$/.test(String(m.maxColors)) ? "" : "s"} max</strong>
                {maxArea > 0 && <> · zone de marquage jusqu'à <strong>{Math.round(maxArea)} cm²</strong></>}
                {" "}· {filteredOut} combinaison{filteredOut > 1 ? "s" : ""} fournisseur masquée{filteredOut > 1 ? "s" : ""}.
              </div>
            )}
            <div className="text-[11px] text-subtle">Les colonnes « prix de vente » sont éditables — modifiez puis cliquez « Enregistrer les prix ».</div>
            {shownMatrix.map((c, i) => {
              const isOpen = openCombo === i;
              const e = edit[i] || { setup: "", tiers: {} };
              return (
                <div key={i} className="border rounded" style={{ borderColor: "var(--border)" }}>
                  <div className="w-full px-3 py-2 sunken text-[11.5px] flex items-center gap-4">
                    <button type="button" onClick={() => setOpenCombo(isOpen ? -1 : i)} className="flex items-center gap-1.5 hover:text-app self-center">
                      <Icon name={isOpen ? "chevronDown" : "chevronRight"} size={12} className="text-subtle shrink-0"/>
                      <span>Couleurs : <strong>{c.colors || "—"}</strong></span>
                    </button>
                    {c.logoSizeCm2 > 0 && <span className="text-muted self-center">Logo : <strong className="text-app">{c.logoSizeCm2} cm²</strong></span>}
                    <div className="ml-auto flex items-start gap-5">
                      <div>
                        <div className="text-[10px] uppercase tracking-wide text-subtle mb-0.5">Setup achat</div>
                        <div className="mono tabular-nums">{fmt.eur(c.setupCharge)}</div>
                      </div>
                      <div>
                        <div className="text-[10px] uppercase tracking-wide text-subtle mb-0.5">Setup vente</div>
                        <Input value={e.setup} onChange={(ev) => setCell(i, "setup", null, ev.target.value)}
                               className="mono text-right w-[100px] inline-block"/>
                      </div>
                    </div>
                  </div>
                  {isOpen && (
                    <table className="axlu-table">
                      <thead><tr>
                        <th>À partir de qté</th>
                        <th className="text-right">Coût PF achat / u.</th>
                        <th className="text-right" style={{ width: 180 }}>Prix vente / u. (éditable)</th>
                      </tr></thead>
                      <tbody>
                        {(c.tiers || []).map((t, j) => (
                          <tr key={j}>
                            <td className="mono text-[12px]">{t.fromQty}+</td>
                            <td className="text-right mono tabular-nums text-muted">{fmt.eur(t.price)}</td>
                            <td className="text-right">
                              <Input value={(e.tiers && e.tiers[j]) || ""} onChange={(ev) => setCell(i, "tier", j, ev.target.value)}
                                     className="mono text-right w-[120px] inline-block"/>
                            </td>
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  )}
                </div>
              );
            })}
          </div>
        )}
      </div>
    </div>
  );
}

// ─── Pricing tab ───────────────────────────────────────────────────────
function ProductPricingTab({ product, manualPrice, setManualPrice }) {
  const v = useAxluStore();
  const toast = useToast();
  const [simQty, setSimQty] = useState(100);
  const [simMarking, setSimMarking] = useState("");
  const [simCombo, setSimCombo] = useState(0);
  const [newTier, setNewTier] = useState({ fromQty: "", netPrice: "" });
  const buyPrice = product.buyPrice || 0;
  const debounceRef = useRef(null);
  const firstRunRef = useRef(true);
  // Saisie libre du prix manuel : on garde le TEXTE brut (gère « 0 », « 0. », « 0,7 ») et on ne parse
  // en nombre que pour la valeur stockée — sinon parseFloat à chaque frappe rendait « 0 »/virgule impossibles.
  const [priceText, setPriceText] = useState(manualPrice != null ? String(manualPrice) : "");
  useEffect(() => { setPriceText(product.manualPrice != null ? String(product.manualPrice) : ""); }, [product.id]);

  useEffect(() => {
    if (firstRunRef.current) {
      firstRunRef.current = false;
      return;
    }
    if (debounceRef.current) clearTimeout(debounceRef.current);
    debounceRef.current = setTimeout(() => {
      window.AxluStore.dispatch('product.update', { id: product.id, patch: { manualPrice } });
    }, 400);
    return () => debounceRef.current && clearTimeout(debounceRef.current);
  }, [manualPrice, product.id]);

  // ── Base sell price — NO transport here. Le transport est facturé au client
  // sous forme de frais de port selon le montant du panier (écran Pricing). ──
  const costs = window.AxluData.PRICING_COSTS || {};
  const baseCoef = costs.baseCoef || 2.5;
  const sellFromBuy = (buy) => {
    const b = Number(buy) || 0;
    return b > 0 ? b * baseCoef : 0;
  };
  const computedRaw = buyPrice * baseCoef;
  const computed = sellFromBuy(buyPrice);

  // ── Supplier price tiers: real PF tiers + manually-added tiers. ──
  const firstVariant = (product.variants || [])[0] || {};
  const pfTiers = (firstVariant.priceTiers && firstVariant.priceTiers.length > 0)
    ? firstVariant.priceTiers
    : (product.priceTiers || []);
  const manualTiers = product.manualPriceTiers || [];
  const allTiers = [
    ...pfTiers.map((t) => ({ ...t, manual: false })),
    ...manualTiers.map((t) => ({ ...t, manual: true })),
  ].sort((a, b) => (a.fromQty || 0) - (b.fromQty || 0));

  // Applicable tier for a quantity: largest fromQty ≤ qty.
  const tierFor = (qty, tiers) => {
    let best = null;
    for (const t of (tiers || [])) {
      if ((t.fromQty || 0) <= qty && (!best || t.fromQty > best.fromQty)) best = t;
    }
    return best || (tiers && tiers[0]) || null;
  };
  const applicable = tierFor(simQty, allTiers);

  const addManualTier = () => {
    const q = parseInt(newTier.fromQty, 10);
    const p = parseFloat(newTier.netPrice);
    if (!q || q <= 0 || !p || p <= 0) {
      toast("Quantité et prix d'achat valides requis", { tone: "warning" });
      return;
    }
    if (allTiers.some((t) => t.fromQty === q)) {
      toast(`Un palier ${q}+ existe déjà`, { tone: "warning" });
      return;
    }
    window.AxluStore.dispatch("product.update", {
      id: product.id,
      patch: { manualPriceTiers: [...manualTiers, { fromQty: q, netPrice: p }] },
    });
    setNewTier({ fromQty: "", netPrice: "" });
    toast(`Palier ${q}+ ajouté`, { tone: "success" });
  };
  const removeManualTier = (fromQty) => {
    window.AxluStore.dispatch("product.update", {
      id: product.id,
      patch: { manualPriceTiers: manualTiers.filter((t) => t.fromQty !== fromQty) },
    });
    toast(`Palier ${fromQty}+ supprimé`);
  };

  // ── Marking simulation data. ──
  const PF_PRINT_PRICES = window.AxluData.PF_PRINT_PRICES || [];
  const MARKING_COEFS = window.AxluData.MARKING_COEFS || {};
  const globalMarkCoef = costs.markingCoef || 2;

  const markingMethods = buildMarkingGroups(product);
  const selMethod = simMarking ? markingMethods.find((mm) => mm.printCode === simMarking) : null;
  const selPriceEntry = simMarking ? PF_PRINT_PRICES.find((p) => p.printCode === simMarking) : null;
  const simCombos = (selMethod && selPriceEntry)
    ? productPriceMatrix(selPriceEntry, selMethod.maxColors, selMethod.zones)
    : [];
  const activeCombo = simCombos.length ? simCombos[Math.min(simCombo, simCombos.length - 1)] : null;

  let markUnit = 0, markSetup = 0;
  if (simMarking && activeCombo) {
    const coef = (MARKING_COEFS[simMarking] != null) ? MARKING_COEFS[simMarking] : globalMarkCoef;
    const override = (product.markingPriceOverrides || {})[simMarking];
    const ov = override && override[comboKey(activeCombo)];
    markSetup = (ov && ov.setupSell != null) ? ov.setupSell : (activeCombo.setupCharge || 0) * coef;
    const mt = tierFor(simQty, activeCombo.tiers || []);
    if (mt) {
      const ovTier = ov && ov.tiers ? ov.tiers[mt.fromQty] : null;
      markUnit = (ovTier != null) ? ovTier : (Number(mt.price) || 0) * coef;
    }
  }

  // ── Final-price simulation. ──
  const baseBuyUnit = applicable ? (Number(applicable.netPrice) || 0) : buyPrice;
  const baseSellUnit = manualPrice != null ? manualPrice : sellFromBuy(baseBuyUnit);
  const unitTotal = baseSellUnit + markUnit;
  const grandTotal = unitTotal * simQty + markSetup;
  const unitAllIn = simQty > 0 ? grandTotal / simQty : unitTotal;
  const marginUnit = baseSellUnit - baseBuyUnit;
  const marginPct = baseSellUnit > 0 ? (marginUnit / baseSellUnit) * 100 : 0;

  return (
    <div className="grid grid-cols-3 gap-6">
      <div className="col-span-2 space-y-4">
        <Card title="Calcul du prix de vente — produit nu">
          <div className="space-y-3">
            <FormulaRow label="Prix achat fournisseur" value={fmt.eur(buyPrice)} muted/>
            <FormulaRow label={`× Coefficient de base (${baseCoef})`} value={`= ${fmt.eur(computedRaw)}`} muted/>
            <div className="border-t pt-3" style={{ borderColor: "var(--border)" }}>
              <FormulaRow label="Prix calculé automatiquement" value={fmt.eur(computed)} bold/>
            </div>
            <div className="sunken rounded-md p-3 text-[12px] text-muted">
              Coût du produit nu, <strong>hors marquage</strong> et <strong>hors transport</strong>.
              Les frais de port sont facturés au client selon le montant du panier (écran <a href="#/pricing" className="accent-fg hover:underline">Pricing</a>).
              Les marquages s'ajoutent via les <a href="#/pricing" className="accent-fg hover:underline">grilles de marquage</a>.
            </div>
          </div>
        </Card>

        <Card title="Prix manuel (override)">
          <div className="text-[12px] text-muted mb-3">Saisir un prix de vente manuel uniquement si le prix calculé n'est pas adapté. Le prix manuel a priorité.</div>
          <div className="flex items-center gap-2">
            <Input value={priceText}
                   onChange={(e) => { const raw = e.target.value; setPriceText(raw); const t = raw.trim().replace(",", "."); if (t === "") setManualPrice(null); else { const n = parseFloat(t); if (Number.isFinite(n)) setManualPrice(n); } }}
                   placeholder={fmt.eur(computed)} className="mono w-[200px]"/>
            {manualPrice != null && (
              <Button variant="ghost" size="sm" onClick={() => { setManualPrice(null); setPriceText(""); }}>Réinitialiser au calculé</Button>
            )}
          </div>
        </Card>

        <Card title="Paliers de prix fournisseur" padding="md">
          <div className="text-[12px] text-muted mb-3">
            Paliers dégressifs tels que reçus de PF Concept. Des paliers supplémentaires peuvent
            être ajoutés manuellement. Prix de vente AXLU = prix d'achat × coefficient de base ({baseCoef}).
          </div>
          {product.moq > 1 && (
            <div className="sunken rounded-md px-3 py-2 mb-3 text-[12px] flex items-center gap-2">
              <Icon name="info" size={14} className="shrink-0 accent-fg"/>
              <span>Minimum de commande fournisseur : <strong>{fmt.num(product.moq)} unités</strong> — toute commande inférieure est refusée par PF Concept.</span>
            </div>
          )}
          {allTiers.length === 0 ? (
            <div className="sunken rounded p-3 text-[12px] text-muted">
              Aucun palier de prix importé. Lancez le flux <strong>Prix</strong> du fournisseur, ou ajoutez un palier manuellement ci-dessous.
            </div>
          ) : (
            <table className="axlu-table">
              <thead>
                <tr>
                  <th>À partir de qté</th>
                  <th>Source</th>
                  <th className="text-right">P. achat unit.</th>
                  <th className="text-right">Prix conseillé PF</th>
                  <th className="text-right">P. vente AXLU calculé</th>
                  <th className="text-right">Marge unit.</th>
                  <th style={{ width: 36 }}></th>
                </tr>
              </thead>
              <tbody>
                {allTiers.map((t, i) => {
                  const ub = Number(t.netPrice) || 0;
                  const industry = Number(t.industryPrice) || 0;
                  const us = sellFromBuy(ub);
                  const isApplicable = applicable && t.fromQty === applicable.fromQty;
                  return (
                    <tr key={`${t.manual ? "m" : "p"}-${t.fromQty}-${i}`} className={isApplicable ? "row-selected" : ""}>
                      <td className="mono">{t.fromQty}+</td>
                      <td>{t.manual ? <Badge tone="warning">Manuel</Badge> : <Badge tone="neutral">PF Concept</Badge>}</td>
                      <td className="text-right mono tabular-nums">{fmt.eur(ub)}</td>
                      <td className="text-right mono tabular-nums text-indigo-600" title="Prix de vente conseillé par PF Concept">{industry > 0 ? fmt.eur(industry) : "—"}</td>
                      <td className="text-right mono tabular-nums font-medium">{fmt.eur(us)}</td>
                      <td className="text-right mono tabular-nums">{fmt.eur(us - ub)} ({us > 0 ? ((us - ub) / us * 100).toFixed(0) : 0}%)</td>
                      <td className="text-right">
                        {t.manual && <IconButton icon="trash" size="sm" onClick={() => removeManualTier(t.fromQty)}/>}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          )}
          <div className="mt-3 pt-3 border-t flex items-end gap-2 flex-wrap" style={{ borderColor: "var(--border)" }}>
            <Field label="À partir de qté">
              <Input value={newTier.fromQty} onChange={(e) => setNewTier({ ...newTier, fromQty: e.target.value })}
                     className="mono w-[120px]" placeholder="500"/>
            </Field>
            <Field label="Prix d'achat unit. (€)">
              <Input value={newTier.netPrice} onChange={(e) => setNewTier({ ...newTier, netPrice: e.target.value })}
                     className="mono w-[140px]" placeholder="4.20"/>
            </Field>
            <Button size="sm" variant="secondary" leftIcon="plus" onClick={addManualTier}>Ajouter un palier</Button>
          </div>
        </Card>
      </div>

      <div>
        <Card title="Simulateur de prix de vente final" padding="md">
          <div className="space-y-3">
            <Field label="Quantité commandée">
              <Input value={simQty} onChange={(e) => {
                const n = parseInt(e.target.value, 10);
                setSimQty(Number.isFinite(n) && n > 0 ? n : 1);
              }} className="mono"/>
            </Field>
            {product.moq > 1 && simQty < product.moq && (
              <div className="text-[11.5px] text-amber-600 -mt-1">
                Sous le minimum de commande du fournisseur ({fmt.num(product.moq)} unités).
              </div>
            )}
            {allTiers.length > 0 && (
              <div className="flex flex-wrap gap-1">
                {allTiers.map((t) => (
                  <button key={`${t.manual ? "m" : "p"}-${t.fromQty}`} onClick={() => setSimQty(t.fromQty)}
                          className={`px-2 py-0.5 text-[11px] mono rounded ${(applicable && t.fromQty === applicable.fromQty) ? "accent-bg accent-fg" : "sunken text-muted hover:text-app"}`}>
                    {t.fromQty}+
                  </button>
                ))}
              </div>
            )}
            <Field label="Marquage">
              <Select value={simMarking}
                      onChange={(val) => { setSimMarking(val); setSimCombo(0); }}
                      options={[
                        { value: "", label: markingMethods.length ? "Aucun marquage (produit nu)" : "Aucune technique importée" },
                        ...markingMethods.map((mm) => ({ value: mm.printCode, label: mm.method || mm.printCode })),
                      ]}/>
            </Field>
            {simMarking && simCombos.length > 1 && (
              <Field label="Combinaison de couleurs">
                <Select value={String(simCombo)}
                        onChange={(val) => setSimCombo(parseInt(val, 10) || 0)}
                        options={simCombos.map((c, i) => ({
                          value: String(i),
                          label: `${c.colors || `Combinaison ${i + 1}`}${c.logoSizeCm2 > 0 ? ` · ${c.logoSizeCm2} cm²` : ""}`,
                        }))}/>
              </Field>
            )}
            {simMarking && !activeCombo && (
              <div className="text-[11.5px] text-amber-600">Aucun barème de prix importé pour cette technique de marquage.</div>
            )}

            <div className="border-t pt-3 space-y-2" style={{ borderColor: "var(--border)" }}>
              <Row k={`Prix produit / u.${applicable ? ` (palier ${applicable.fromQty}+)` : ""}`} mono>{fmt.eur(baseSellUnit)}</Row>
              {manualPrice != null && <div className="text-[11px] text-amber-600 -mt-1">Prix manuel appliqué (override).</div>}
              {simMarking && activeCombo && <Row k="Marquage / u." mono>{fmt.eur(markUnit)}</Row>}
              <Row k="Prix de vente unitaire" mono><strong>{fmt.eur(unitTotal)}</strong></Row>
              {simMarking && activeCombo && markSetup > 0 && (
                <Row k="Frais de setup (unique)" mono>{fmt.eur(markSetup)}</Row>
              )}
            </div>

            <div className="text-center py-3 sunken rounded-md">
              <div className="text-[10px] uppercase tracking-wide text-muted">Total pour {fmt.num(simQty)} pièce{simQty > 1 ? "s" : ""}</div>
              <div className="text-[28px] font-semibold mono tabular-nums mt-1">{fmt.eur(grandTotal)}</div>
              <div className="text-[11px] text-muted mt-0.5">soit {fmt.eur(unitAllIn)} / pièce tout compris</div>
            </div>

            <div className="border-t pt-2" style={{ borderColor: "var(--border)" }}>
              <Row k="Marge produit / u." mono>
                <span className={marginPct < 50 ? "text-amber-600" : "text-emerald-600"}>{fmt.eur(marginUnit)}</span>
              </Row>
              <Row k="Marge produit %" mono>
                <span className={marginPct < 50 ? "text-amber-600" : "text-emerald-600"}>{fmt.pct(marginPct)}</span>
              </Row>
            </div>
            <div className="text-[11px] text-subtle">
              Marge calculée sur le produit nu. Le marquage et les frais de port sont facturés en plus au client.
            </div>
          </div>
        </Card>
      </div>
    </div>
  );
}

function FormulaRow({ label, value, muted, bold }) {
  return (
    <div className="flex items-center justify-between text-[13px]">
      <span className={muted ? "text-muted" : ""}>{label}</span>
      <span className={`mono tabular-nums ${bold ? "text-[16px] font-semibold" : ""}`}>{value}</span>
    </div>
  );
}

// ─── Stock tab ─────────────────────────────────────────────────────────
// Format a YYYY-MM-DD (or any parseable) date as DD/MM/YYYY; fall back to raw.
function fmtShortDate(s) {
  if (!s) return "";
  const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(s));
  if (m) return `${m[3]}/${m[2]}/${m[1]}`;
  const d = new Date(s);
  return isNaN(d.getTime()) ? String(s) : d.toLocaleDateString("fr-FR");
}

// Reappro cell — next purchase-order quantity + expected date (PF stock feed).
function ReapproCell({ vr }) {
  const qty = vr.stockNextPo || 0;
  const date = vr.stockDateNextPo;
  const future = vr.stockFuture || 0;
  if (!qty && !date && !future) {
    return <span className="text-subtle">{(vr.stock || 0) > 0 ? "—" : "Aucun réappro communiqué"}</span>;
  }
  return (
    <span className="inline-flex items-center gap-1.5">
      {qty > 0
        ? <span className="mono tabular-nums text-emerald-600">+{fmt.num(qty)} u.</span>
        : (future > 0 ? <span className="mono tabular-nums">{fmt.num(future)} u. à venir</span> : null)}
      {date && <span className="text-muted">attendu le {fmtShortDate(date)}</span>}
    </span>
  );
}

function ProductStockTab({ product }) {
  const v = useAxluStore();
  const variants = product.variants || [];
  const hasSizes = variants.some((vr) => vr.size);
  const totalStock = variants.reduce((s, vr) => s + (vr.stock || 0), 0);
  const totalIncoming = variants.reduce((s, vr) => s + (vr.stockNextPo || 0), 0);
  const allUncounted = variants.length > 0 && variants.every((vr) => vr.stockUncounted);

  return (
    <Card padding="none" title="Stock par variante" action={
      <div className="flex items-center gap-2">
        <Badge tone={(totalStock > 0 || allUncounted) ? "success" : "danger"}>
          {allUncounted ? "Disponible" : `${fmt.num(totalStock)} en stock`}
        </Badge>
        {totalIncoming > 0 && <Badge tone="info">+{fmt.num(totalIncoming)} en réappro</Badge>}
      </div>
    }>
      <div className="px-4 py-2 text-[12px] text-muted border-b" style={{ borderColor: "var(--border)" }}>
        Stock disponible, délais de réapprovisionnement et entrepôt tels que reçus du flux <strong>Stock</strong> de PF Concept.
      </div>
      {variants.length === 0 ? (
        <div className="p-6">
          <EmptyState icon="catalog" title="Aucune variante" description="Ce produit n'a pas encore de variantes."/>
        </div>
      ) : (
        <table className="axlu-table">
          <thead>
            <tr>
              <th>SKU</th>
              <th>Couleur</th>
              {hasSizes && <th>Taille</th>}
              <th>Code EAN</th>
              <th>Disponible</th>
              <th>Statut</th>
              <th>Réapprovisionnement</th>
              <th>Lieu de stock</th>
            </tr>
          </thead>
          <tbody>
            {variants.map((vr, i) => (
              <tr key={vr.sku || i} className="hover-row">
                <td className="mono text-[12px]">{vr.sku}</td>
                <td>
                  <div className="flex items-center gap-2">
                    <ColorDot hex={vr.colorHex} name={vr.color}/>
                    <span className="text-[12.5px]">{vr.color || "—"}</span>
                  </div>
                </td>
                {hasSizes && <td className="mono text-[12px]">{vr.size || "—"}</td>}
                <td className="mono text-[12px] text-muted">{vr.ean || "—"}</td>
                <td className={`mono tabular-nums ${vr.stockUncounted ? "text-emerald-600" : (vr.stock || 0) === 0 ? "text-red-600" : (vr.stock || 0) < 50 ? "text-amber-600" : ""}`}>
                  {stockText(vr.stock, vr.stockUncounted)}
                </td>
                <td>{stockBadge(vr.stock || 0, vr.stockUncounted)}</td>
                <td className="text-[12px]"><ReapproCell vr={vr}/></td>
                <td className="text-[12px] text-muted">{vr.stockLocation || "—"}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </Card>
  );
}

// ─── Shopify tab ───────────────────────────────────────────────────────
function ProductShopifyTab({ product, status }) {
  const v = useAxluStore();
  const toast = useToast();
  const [busy, setBusy] = useState(false);
  const [store, setStore] = useState("");
  React.useEffect(() => {
    if (window.axlu && window.axlu.shopify) {
      window.axlu.shopify.getConfig().then((c) => { if (c) setStore(c.shop || ""); }).catch(() => {});
    }
  }, []);

  const onShopify = !!product.shopifyId;
  const isActive = product.shopifyPub === "active" && onShopify;
  const isDraft = product.shopifyPub === "draft" && onShopify;
  const hasError = product.shopifyPub === "error" || !!product.shopifyError;
  const numericId = product.shopifyId ? String(product.shopifyId).split("/").pop() : null;

  const doPublish = async () => {
    if (busy) return;
    setBusy(true);
    toast(`Publication de ${product.sku}…`, { tone: "info" });
    try {
      const r = await window.AxluStore.shopifyPublish(product.id);
      if (r && r.ok) {
        const sw = r.stock && r.stock.ok === false ? ` · stock non poussé : ${r.stock.error || "erreur"}` : "";
        toast(`${product.sku} publié sur Shopify ✓${sw}`, { tone: sw ? "warning" : "success" });
      } else {
        toast(`Échec : ${(r && r.error) || "erreur"}`, { tone: "danger" });
      }
    } finally { setBusy(false); }
  };
  const doUnpublish = async () => {
    if (busy) return;
    setBusy(true);
    try {
      const r = await window.AxluStore.shopifyUnpublish(product.id);
      toast(r && r.ok ? `${product.sku} retiré de Shopify` : `Échec : ${(r && r.error) || "erreur"}`, { tone: r && r.ok ? "warning" : "danger" });
    } finally { setBusy(false); }
  };
  const doActivate = async () => {
    if (busy) return;
    setBusy(true);
    try {
      const r = await window.AxluStore.shopifyActivate(product.id);
      toast(r && r.ok ? `${product.sku} activé sur Shopify ✓` : `Échec : ${(r && r.error) || "erreur"}`, { tone: r && r.ok ? "success" : "danger" });
    } finally { setBusy(false); }
  };
  const doDeactivate = async () => {
    if (busy) return;
    setBusy(true);
    try {
      const r = await window.AxluStore.shopifyDeactivate(product.id);
      toast(r && r.ok ? `${product.sku} désactivé (brouillon)` : `Échec : ${(r && r.error) || "erreur"}`, { tone: r && r.ok ? "warning" : "danger" });
    } finally { setBusy(false); }
  };
  const openInShopify = () => {
    if (!store || !numericId) return;
    const handle = store.replace(".myshopify.com", "");
    window.open(`https://admin.shopify.com/store/${handle}/products/${numericId}`, "_blank");
  };

  return (
    <div className="grid grid-cols-3 gap-6">
      <div className="col-span-2 space-y-4">
        <Card title="État de la publication">
          {!store && (
            <div className="mb-3 text-[12px] rounded-md px-3 py-2 sunken text-amber-600">
              Aucune boutique connectée — connectez Shopify dans <a href="#/shopify" className="accent-fg hover:underline">Publications → Configuration</a>.
            </div>
          )}
          <div className="grid grid-cols-2 gap-4">
            <Row k="Statut">
              <ShopifyBadge pub={product.shopifyPub} />
            </Row>
            <Row k="ID Shopify" mono>{numericId || "—"}</Row>
            <Row k="Dernière publication" mono>{product.lastPublishedAt ? fmt.date(product.lastPublishedAt) : "—"}</Row>
            <Row k="Boutique" mono>{store || "—"}</Row>
            <Row k="Catégorie">{product.categoryLabel || "—"}</Row>
            <Row k="Variantes" mono>{(product.variants || []).length}</Row>
          </div>

          {hasError && product.shopifyError && (
            <div className="mt-3 text-[12px] rounded-md px-3 py-2 sunken text-red-600 dark:text-red-400">
              <strong>Erreur Shopify :</strong> {product.shopifyError}
            </div>
          )}

          <div className="mt-4 flex gap-2 flex-wrap">
            <Button variant="primary" size="sm" leftIcon="cart" onClick={doPublish} disabled={busy || !store}>
              {busy ? "En cours…" : ((isDraft || isActive) ? "Forcer une mise à jour" : "Publier sur Shopify")}
            </Button>
            {!isActive && (
              <Button variant="primary" size="sm" leftIcon="check" onClick={doActivate} disabled={busy}>Activer</Button>
            )}
            {isActive && (
              <Button variant="ghost" size="sm" leftIcon="eyeOff" onClick={doDeactivate} disabled={busy}>Désactiver</Button>
            )}
            {(isDraft || isActive) && (
              <Button variant="secondary" size="sm" leftIcon="link" onClick={openInShopify} disabled={!numericId}>Voir sur la boutique</Button>
            )}
            {(isDraft || isActive) && (
              <Button variant="ghost" size="sm" leftIcon="eyeOff" onClick={doUnpublish} disabled={busy}>Dépublier</Button>
            )}
          </div>
        </Card>

        <Card title="Historique de publication" padding="none">
          {(() => {
            const audit = window.AxluData.AUDIT_LOG || [];
            const events = audit.filter((a) =>
              (a.entity === 'product' || a.entity === 'publication') &&
              a.target && (String(a.target).includes(product.sku) || String(a.target).includes(product.id))
            );
            if (events.length === 0) {
              return <div className="px-4 py-6 text-center text-[12px] text-muted">Aucun évènement de publication pour ce produit.</div>;
            }
            return (
              <table className="axlu-table">
                <thead>
                  <tr><th>Date</th><th>Action</th><th>Utilisateur</th></tr>
                </thead>
                <tbody>
                  {events.map((r, i) => (
                    <tr key={i}>
                      <td className="mono text-[12px] text-muted">{fmt.date(r.ts)}</td>
                      <td>{r.action}</td>
                      <td>{r.user}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            );
          })()}
        </Card>
      </div>

      <div>
        <Card title="Ce qui est envoyé à Shopify" padding="md">
          <div className="text-[12px] text-muted space-y-1.5">
            <div>• Titre, description, marque, catégorie</div>
            <div>• Variantes (Couleur / Taille), SKU, prix, EAN</div>
            <div>• Images produit</div>
            <div>• Metafields <span className="mono text-[11px]">axlu.*</span> : caractéristiques, couleurs (hex), marquages + photos de zones, prix dégressifs</div>
          </div>
          <div className="text-[11px] text-subtle mt-2 pt-2 border-t" style={{ borderColor: "var(--border)" }}>
            Stocks : nécessite le scope <span className="mono">write_inventory</span> (à ajouter à l'app). Images par variante : à venir.
          </div>
        </Card>
      </div>
    </div>
  );
}
function edited(_, v) { return v; }

// ─── History tab ───────────────────────────────────────────────────────
function ProductHistoryTab({ product }) {
  const v = useAxluStore();
  const [query, setQuery] = useState('');

  const events = useMemo(() => {
    const audit = window.AxluData.AUDIT_LOG || [];
    const productEvents = audit
      .filter((a) => (a.target && String(a.target).includes(product.sku)) || (a.entity === 'product' && String(a.target).includes(product.id)))
      .map((a) => ({
        ts: a.ts || a.timestamp,
        user: a.user || a.author || 'Système',
        action: a.action || a.label || '—',
        detail: a.detail || a.target || '',
      }));
    if (!query.trim()) return productEvents;
    const q = query.toLowerCase();
    return productEvents.filter((e) =>
      (e.user || '').toLowerCase().includes(q) ||
      (e.action || '').toLowerCase().includes(q) ||
      (e.detail || '').toLowerCase().includes(q)
    );
  }, [v, query, product.sku, product.id]);

  return (
    <Card title="Audit log produit" padding="none" action={
      <Input size="sm" leftIcon="search" placeholder="Filtrer…" className="w-[180px]"
             value={query} onChange={(e) => setQuery(e.target.value)}/>
    }>
      <div className="px-4">
        {events.length === 0 && (
          <div className="py-6 text-center text-[12px] text-muted">Aucune entrée correspondante.</div>
        )}
        {events.map((e, i) => (
          <div key={i} className="flex gap-4 py-3 border-b last:border-b-0" style={{ borderColor: "var(--border)" }}>
            <Avatar name={e.user} size={32}/>
            <div className="flex-1">
              <div className="text-[13px]">
                <span className="font-medium">{e.user}</span>{" "}
                <span className="text-muted">— {e.action}</span>
              </div>
              <div className="text-[12px] text-muted mt-0.5 mono">{e.detail}</div>
            </div>
            <div className="text-[11px] text-subtle whitespace-nowrap mono self-start">{fmt.date(e.ts)}</div>
          </div>
        ))}
      </div>
    </Card>
  );
}

Object.assign(window, { ProductDetail });
