import { useEffect, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { api } from "../api";
import { useConfirm } from "../confirm";
import { useAuth } from "../auth";
import BarcodeList from "../components/BarcodeList";
import Icon from "../components/Icon";
import OffVergleich from "../components/OffVergleich";
import ProduktBild from "../components/ProduktBild";
import { useToast } from "../toast";
import { useSettings } from "../settings";
import { asTree } from "../categoryTree";
import CategorySelect from "../components/CategorySelect";
import LocationMinStock from "../components/LocationMinStock";
import { DynamicFields, FIELD_TYPES } from "../fields";
import ObjektBestand from "../components/ObjektBestand";
import Einzelstuecke from "../components/Einzelstuecke";
import {
  daysUntil, expiryRowClass, fmt, fromMonthInput, gebinde, isExpired, relativeExpiry,
  toMonthInput, unitShort,
} from "../units";

const EMPTY = {
  barcode: "", name: "", brand: "", image_url: "",
  unit_id: "", package_size: "", package_label: "", date_precision: "day", min_stock: "",
  group_id: "", category_id: "", shop_id: "", product_url: "", individual: false,
};

// (Gebinde kommen jetzt aus der Verwaltung, siehe api.listPackageTypes)

// Kanonische Einheit je Basiseinheit (für OFF-Vorschläge und Altbestand).
const CANONICAL_NAME = { piece: "Stück", gram: "Gramm", milliliter: "Milliliter" };

const KIND_LABEL = { count: "Anzahl", weight: "Gewicht", volume: "Volumen" };
const KIND_ORDER = ["count", "weight", "volume"];

// Foto vor dem Hochladen verkleinern: kleiner in der DB, schneller im Upload und
// zuverlässig unter dem Server-Limit. Bei Problemen fällt es aufs Original zurück.
function verkleinereBild(datei, maxDim = 1600, quality = 0.85) {
  return new Promise((resolve) => {
    if (!datei || !datei.type?.startsWith("image/")) return resolve(datei);
    const url = URL.createObjectURL(datei);
    const img = new Image();
    img.onload = () => {
      URL.revokeObjectURL(url);
      const faktor = Math.min(1, maxDim / Math.max(img.width, img.height));
      if (faktor >= 1 && datei.size <= 1_500_000) return resolve(datei);
      const w = Math.round(img.width * faktor);
      const h = Math.round(img.height * faktor);
      const canvas = document.createElement("canvas");
      canvas.width = w;
      canvas.height = h;
      canvas.getContext("2d").drawImage(img, 0, 0, w, h);
      canvas.toBlob(
        (blob) => resolve(blob ? new File([blob], "foto.jpg", { type: "image/jpeg" }) : datei),
        "image/jpeg", quality,
      );
    };
    img.onerror = () => { URL.revokeObjectURL(url); resolve(datei); };
    img.src = url;
  });
}

export default function ProductForm() {
  const confirm = useConfirm();
  const toast = useToast();
  const { id } = useParams();
  const isNew = !id;
  const { isAdmin } = useAuth();
  const navigate = useNavigate();
  const [searchParams] = useSearchParams();

  const [form, setForm] = useState(EMPTY);
  const [product, setProduct] = useState(null);
  const [lots, setLots] = useState([]);
  const [groups, setGroups] = useState([]);
  const [categories, setCategories] = useState([]);
  const [units, setUnits] = useState([]);
  const [gebindearten, setGebindearten] = useState([]);
  const [locations, setLocations] = useState([]);
  const [shops, setShops] = useState([]);
  // Gegenstände: effektive (vererbte) Felder der Kategorie + deren Werte am Artikel.
  const [effFields, setEffFields] = useState([]);
  const [fieldValues, setFieldValues] = useState({});
  // Umschalter Lebensmittel/Gegenstand: filtert die Kategorieauswahl auf den Typ.
  // Neuanlage startet als Gegenstand (der häufigere Non-Food-Fall); beim Bearbeiten
  // wird er aus dem Artikel gesetzt.
  const [modus, setModus] = useState("object");
  // Direkt im Formular neue Kategorien/Felder anlegen.
  const [newCat, setNewCat] = useState(null);     // { name, parent_id } | null
  const [newField, setNewField] = useState(null); // { label, field_type, unit, options, required } | null
  const [error, setError] = useState(null);
  const [busy, setBusy] = useState(false);
  // Einheit für die Mindestbestand-Eingabe: "package" oder die ID einer Einheit.
  const [minUnit, setMinUnit] = useState("");
  const [warnDays, setWarnDays] = useState(7);
  // Offene Gegenüberstellung mit Open Food Facts (null = zu).
  const [offDaten, setOffDaten] = useState(null);
  const [offBusy, setOffBusy] = useState(false);
  const [bildVersion, setBildVersion] = useState(0);
  // Ob gerade ein Bild vorliegt – steuert, ob „Entfernen" angeboten wird.
  const [hatBild, setHatBild] = useState(false);

  const selectedUnit = units.find((u) => String(u.id) === String(form.unit_id)) || null;
  const unitFactor = selectedUnit ? selectedUnit.factor : 1;
  const unitName = selectedUnit ? selectedUnit.name : "";

  // Der Umschalter bestimmt die Verwaltungsart; die Kategorieliste ist darauf
  // gefiltert, also passt die gewählte Kategorie immer zum Modus.
  const isObject = modus === "object";
  // Gegenstände gleicht das Backend gegen Open Products Facts ab, Lebensmittel
  // gegen Open Food Facts – die Beschriftung folgt dem Modus.
  const datenquelle = isObject ? "Open Products Facts" : "Open Food Facts";

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

  // Typ umschalten: passt die gewählte Kategorie nicht mehr, wird sie geleert.
  function changeModus(neu) {
    setModus(neu);
    const cat = categories.find((c) => String(c.id) === String(form.category_id));
    if (cat && cat.tracking !== neu) {
      set("category_id", "");
      setEffFields([]);
    }
  }

  function setField(fieldId, value) {
    setFieldValues((v) => ({ ...v, [fieldId]: value }));
  }

  // Öffnet den Inline-Dialog „Neue Kategorie" – als Unterkategorie der gerade
  // gewählten (falls eine gewählt ist), im selben Modus.
  function openNewCat() {
    // Neue Kategorie erbt den aktuell gewählten Typ (Umschalter).
    setNewCat({ name: "", parent_id: form.category_id || "", tracking: modus });
  }

  async function createCategoryInline() {
    if (!newCat?.name.trim()) return;
    setError(null);
    try {
      const created = await api.createCategory({
        name: newCat.name.trim(),
        parent_id: newCat.parent_id === "" ? null : Number(newCat.parent_id),
        tracking: newCat.tracking,
      });
      setCategories(await api.listCategories());
      set("category_id", String(created.id));
      setNewCat(null);
      toast(`Kategorie „${created.name}“ angelegt.`);
    } catch (err) {
      setError(err.message);
    }
  }

  async function createFieldInline() {
    if (!newField?.label.trim() || form.category_id === "") return;
    setError(null);
    try {
      await api.createFieldDefinition({
        category_id: Number(form.category_id),
        label: newField.label.trim(),
        field_type: newField.field_type,
        unit: newField.field_type === "number" ? (newField.unit || null) : null,
        options: newField.field_type === "select"
          ? newField.options.split(",").map((o) => o.trim()).filter(Boolean)
          : null,
        required: newField.required,
      });
      setEffFields(await api.categoryFields(Number(form.category_id)));
      setNewField(null);
      toast("Feld hinzugefügt.");
    } catch (err) {
      setError(err.message);
    }
  }

  async function onPickPhoto(e) {
    const file = e.target.files?.[0];
    e.target.value = "";              // erlaubt, dieselbe Datei erneut zu wählen
    if (!file || isNew) return;
    setError(null);
    try {
      const foto = await verkleinereBild(file);
      await api.uploadProductImage(id, foto);
      setBildVersion((v) => v + 1);
      toast("Foto gespeichert.");
    } catch (err) {
      setError(err.message);
    }
  }

  async function removePhoto() {
    const ok = await confirm({
      title: "Foto entfernen?", confirmLabel: "Entfernen", danger: true,
    });
    if (!ok) return;
    try {
      await api.deleteProductImage(id);
      setBildVersion((v) => v + 1);
    } catch (err) {
      setError(err.message);
    }
  }

  function canonicalUnitId(unitList, baseUnit) {
    const name = CANONICAL_NAME[baseUnit];
    const hit = unitList.find((u) => u.name === name);
    return hit ? String(hit.id) : "";
  }

  // Faktor, mit dem der angezeigte Mindestbestand in Basiseinheiten umgerechnet wird.
  // sel ist "package" oder die ID einer verwalteten Einheit.
  function minFactor(sel, pkgSize, unitList) {
    if (sel === "package") return Number(pkgSize) > 0 ? Number(pkgSize) : 1;
    const u = unitList.find((x) => String(x.id) === String(sel));
    return u ? u.factor : 1;
  }

  function changeMinUnit(newSel) {
    if (form.min_stock !== "" && newSel !== minUnit) {
      const oldF = minFactor(minUnit, form.package_size, units);
      const newF = minFactor(newSel, form.package_size, units);
      set("min_stock", String((Number(form.min_stock) * oldF) / newF));
    }
    setMinUnit(newSel);
  }

  function applySuggestion(res, unitList) {
    const s = res.suggestion;
    setForm((f) => ({
      ...f,
      barcode: s.barcode || f.barcode,
      name: s.name || f.name,
      brand: s.brand || f.brand,
      image_url: s.image_url || f.image_url,
      unit_id: f.unit_id || canonicalUnitId(unitList, s.base_unit || "piece"),
      package_size: s.package_size != null ? String(s.package_size) : f.package_size,
      // Kategorie schlaegt das Backend aus der OFF-Einordnung vor; die Gruppe
      // nur, wenn der Code dort ausdruecklich hinterlegt ist.
      category_id: f.category_id || (res.category_id ? String(res.category_id) : ""),
      group_id: f.group_id || (res.group_id ? String(res.group_id) : ""),
    }));
    if (s.package_size != null) setMinUnit("package");
    if (res.category_id) {
      const cat = categories.find((c) => String(c.id) === String(res.category_id));
      if (cat) setModus(cat.tracking);
    }
    toast(
      `Daten von ${datenquelle} übernommen.` +
        (s.package_size != null ? "" : " (Füllmenge nicht hinterlegt – bitte Packungsgröße prüfen.)") +
        (res.category_name ? ` Kategorie „${res.category_name}“ vorgeschlagen.` : "") +
        (res.group_name ? ` Gruppe „${res.group_name}“ über den EAN-Code erkannt.` : "")
    );
  }

  async function runLookup(code, unitList) {
    if (!code) return;
    setError(null);

    try {
      const res = await api.lookup(code.trim());
      if (res.found && res.existing_product) {
        toast("Dieses Produkt existiert bereits.");
        navigate(`/products/${res.existing_product.id}`);
      } else if (res.found && res.suggestion) {
        applySuggestion(res, unitList);
      } else {
        toast("Barcode unbekannt – bitte Daten selbst eingeben.", "warn");
      }
    } catch (err) {
      setError(err.message);
    }
  }

  /**
   * Open Food Facts erneut befragen, obwohl der Artikel schon existiert.
   * Übernommen wird nichts von allein – das entscheidet die Gegenüberstellung.
   */
  async function holeOffDaten() {
    setError(null);
    setOffBusy(true);
    try {
      const res = await api.offCompare(id);
      if (res.found && res.suggestion) {
        setOffDaten({ ...res.suggestion, category_id: res.category_id, category_name: res.category_name });
      } else {
        setOffDaten(null);
        toast(`${datenquelle} kennt keinen der hinterlegten EAN-Codes.`, "warn");
      }
    } catch (err) {
      setError(err.message);
    } finally {
      setOffBusy(false);
    }
  }

  /** Welche Felder die Gegenüberstellung zeigt – und woher ihr Wert kommt. */
  function offFelder() {
    if (!offDaten) return [];
    const kategorie = categories.find((c) => String(c.id) === String(form.category_id));
    return [
      { schluessel: "name", titel: "Name", eigen: form.name, fremd: offDaten.name || "" },
      { schluessel: "brand", titel: "Marke", eigen: form.brand, fremd: offDaten.brand || "" },
      {
        schluessel: "package_size", titel: "Packungsgröße",
        eigen: form.package_size, fremd: offDaten.package_size ?? "",
      },
      {
        schluessel: "category_id", titel: "Kategorie",
        eigen: form.category_id, fremd: offDaten.category_id ?? "",
        anzeigeEigen: kategorie ? kategorie.name : "–",
        anzeigeFremd: offDaten.category_name || "–",
      },
    ];
  }

  function uebernehmen(schluessel) {
    if (!offDaten) return;
    if (schluessel === "image_url") {
      set("image_url", offDaten.image_url || "");
      // Das Bild selbst wechselt erst beim Speichern - ohne Hinweis wirkt der
      // Knopf folgenlos.
      toast("Bild übernommen. Mit „Speichern“ wird es geholt.");
      return;
    }
    if (schluessel === "package_size") {
      set("package_size", offDaten.package_size != null ? String(offDaten.package_size) : "");
      return;
    }
    if (schluessel === "category_id") {
      set("category_id", offDaten.category_id ? String(offDaten.category_id) : "");
      return;
    }
    set(schluessel, offDaten[schluessel] || "");
  }

  function alleUebernehmen() {
    offFelder().forEach((f) => {
      if (f.fremd !== "" && f.fremd !== null && String(f.fremd) !== String(f.eigen)) {
        uebernehmen(f.schluessel);
      }
    });
    if (offDaten?.image_url) uebernehmen("image_url");
  }

  useEffect(() => {
    async function load() {
      try {
        const [gs, us, cs, pts, locs, shs] = await Promise.all([
          api.listGroups(), api.listUnits(), api.listCategories(), api.listPackageTypes(),
          api.listLocations(), api.listShops(),
        ]);
        setGroups(gs);
        setUnits(us);
        setCategories(cs);
        setGebindearten(pts);
        setLocations(locs);
        setShops(shs);
        try {
          const s = await api.listSettings();
          const row = s.find((x) => x.key === "expiry_warning_days");
          if (row) setWarnDays(parseInt(row.value, 10) || 7);
        } catch { /* optional */ }

        if (!isNew) {
          const p = await api.getProduct(id);
          setProduct(p);
          const uid = p.display_unit_id
            ? String(p.display_unit_id)
            : canonicalUnitId(us, p.base_unit);
          // Erfassungseinheit des Mindestbestands wiederherstellen.
          const mode = p.min_stock_in_packages
            ? "package"
            : p.min_stock_unit_id
              ? String(p.min_stock_unit_id)
              : uid;
          const f = minFactor(mode, p.package_size, us);
          setMinUnit(mode);
          setForm({
            barcode: p.barcode || "", name: p.name, brand: p.brand || "",
            image_url: p.image_url || "", unit_id: uid,
            package_size: p.package_size ?? "", package_label: p.package_label || "",
            date_precision: p.date_precision === "month" ? "month" : "day",
            min_stock: p.min_stock != null ? p.min_stock / f : "",
            group_id: p.group_id ?? "",
            category_id: p.category_id ?? "",
            shop_id: p.shop_id ?? "",
            product_url: p.product_url || "",
            individual: Boolean(p.individual),
          });
          setFieldValues(p.field_values || {});
          setModus(p.tracking === "object" ? "object" : "food");
          setLots(await api.listLots(id));
        } else {
          const bc = searchParams.get("barcode");
          if (bc) {
            set("barcode", bc);
            await runLookup(bc, us);
          }
        }
      } catch (err) {
        setError(err.message);
      }
    }
    load();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [id]);

  // Wechselt die Kategorie, die geltenden (vererbten) Felder nachladen –
  // aber nur für Gegenstände. Lebensmittel haben keine eigenen Felder.
  useEffect(() => {
    const cat = categories.find((c) => String(c.id) === String(form.category_id)) || null;
    if (!cat || cat.tracking !== "object") {
      setEffFields([]);
      return;
    }
    let aktiv = true;
    api.categoryFields(cat.id).then((fs) => { if (aktiv) setEffFields(fs); }).catch(() => {});
    return () => { aktiv = false; };
  }, [form.category_id, categories]);

  function buildPayload() {
    const sel = minUnit || form.unit_id;
    let minBase = null;
    if (form.min_stock !== "") {
      minBase = Number(form.min_stock) * minFactor(sel, form.package_size, units);
    }
    const base = {
      barcode: form.barcode || null,
      name: form.name,
      brand: form.brand || null,
      image_url: form.image_url || null,
      category_id: form.category_id === "" ? null : Number(form.category_id),
      shop_id: form.shop_id === "" ? null : Number(form.shop_id),
      product_url: form.product_url.trim() || null,
    };
    if (isObject) {
      // Gegenstände: keine Lebensmittel-Felder (Einheit/Packung/MHD/Gruppe).
      // Nur die geltenden eigenen Felder mitschicken – Werte fremder Kategorien
      // (nach einem Kategoriewechsel) fallen dabei weg.
      const erlaubt = new Set(effFields.map((f) => String(f.id)));
      const fv = {};
      Object.entries(fieldValues).forEach(([k, v]) => {
        if (erlaubt.has(String(k))) fv[k] = v;
      });
      return { ...base, individual: form.individual, field_values: fv };
    }
    return {
      ...base,
      individual: false,
      unit_id: form.unit_id === "" ? null : Number(form.unit_id),
      package_size: form.package_size === "" ? null : Number(form.package_size),
      package_label: form.package_label.trim() || null,
      date_precision: form.date_precision || "day",
      min_stock: minBase,
      min_stock_unit_id: sel === "package" || sel === "" ? null : Number(sel),
      min_stock_in_packages: sel === "package",
      group_id: form.group_id === "" ? null : Number(form.group_id),
    };
  }

  async function save(e) {
    e.preventDefault();
    setError(null);
    setBusy(true);
    try {
      if (isNew) {
        const created = await api.createProduct(buildPayload());
        navigate(`/products/${created.id}`);
      } else {
        await api.updateProduct(id, buildPayload());
        toast("Gespeichert.");
        setProduct(await api.getProduct(id));
        setLots(await api.listLots(id));
        // Der Server holt bei geaenderter Bildadresse ein neues Bild. Ohne
        // diesen Zaehler zeigte die Seite weiter das alte aus dem Zwischenspeicher.
        setBildVersion((v) => v + 1);
        setOffDaten(null);
      }
    } catch (err) {
      setError(err.message);
    } finally {
      setBusy(false);
    }
  }

  async function remove() {
    const ok = await confirm({
      title: "Produkt löschen?",
      message: "Der gesamte Bestand dieses Produkts geht dabei verloren.",
      confirmLabel: "Löschen",
      danger: true,
    });
    if (!ok) return;
    try {
      await api.deleteProduct(id);
      navigate("/products");
    } catch (err) {
      setError(err.message);
    }
  }

  const readOnly = !isAdmin;
  const BASE_SHORT_OF_KIND = { count: "Stk", weight: "g", volume: "ml" };
  const baseShort = selectedUnit
    ? BASE_SHORT_OF_KIND[selectedUnit.kind]
    : product ? unitShort(product.base_unit) : "";

  return (
    <div>
      <div className="page-head">
        <div>
          <h1>{isNew ? "Neues Produkt" : form.name || "Produkt"}</h1>
          {!isNew && product && (
            <div className="sub">
              Bestand: {fmt(product.stock / (product.unit_factor || 1))} {product.unit_name}
              {product.package_size ? ` · ${fmt(product.stock / product.package_size)} Pkg` : ""}
            </div>
          )}
        </div>
        <button className="btn ghost" onClick={() => navigate(-1)}>Zurück</button>
      </div>

      {error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}

      <div className="grid-2">
        <form className="card" onSubmit={save}>
          {/* Bild steht neben den Kennfeldern, nicht darueber: Der Platz rechts
              von Barcode, Name und Marke ist ohnehin selten ausgenutzt. Fehlt
              ein Bild, rendert die Komponente nichts und die Felder nehmen die
              volle Breite ein. */}
          <div className="produkt-kopf">
            <div className="produkt-kopf-felder">
              <div className="field-inline">
                <label className="grow" style={{ flex: "1 1 auto", minWidth: 0 }}>
                  Barcode
                  <input value={form.barcode} onChange={(e) => set("barcode", e.target.value)} disabled={readOnly} />
                </label>
                {isAdmin && (
                  <div className="knopf-spalte">
                    <button type="button" className="btn" onClick={() => runLookup(form.barcode, units)}>
                      <Icon name="search" size={16} />Nachschlagen
                    </button>
                    {!isNew && (
                      <button type="button" className="btn" disabled={offBusy}
                        onClick={holeOffDaten}>
                        <Icon name="download" size={16} />
                        {offBusy ? "Frage an…" : `Mit ${datenquelle} vergleichen`}
                      </button>
                    )}
                  </div>
                )}
              </div>
              <label>
                Name
                <input value={form.name} onChange={(e) => set("name", e.target.value)} required disabled={readOnly} />
              </label>
              <label>
                Marke
                <input value={form.brand} onChange={(e) => set("brand", e.target.value)} disabled={readOnly} />
              </label>
            </div>
            {!isNew && (
              <div className="produkt-foto">
                <ProduktBild productId={id} alt={form.name} version={bildVersion} onLoaded={setHatBild} />
                {isAdmin && (
                  <div className="foto-knoepfe">
                    <label className="btn sm" title="Mit der Kamera aufnehmen">
                      Foto machen
                      <input hidden type="file" accept="image/*" capture="environment" onChange={onPickPhoto} />
                    </label>
                    <label className="btn sm" title="Aus der Galerie wählen">
                      Galerie
                      <input hidden type="file" accept="image/*" onChange={onPickPhoto} />
                    </label>
                    {hatBild && (
                      <button type="button" className="btn sm" onClick={removePhoto}>Entfernen</button>
                    )}
                  </div>
                )}
              </div>
            )}
          </div>

          {offDaten && (
            <OffVergleich
              quelle={datenquelle}
              felder={offFelder()}
              bildEigenId={id}
              bildFremdUrl={offDaten.image_url || ""}
              bildUebernommen={Boolean(offDaten.image_url) && form.image_url === offDaten.image_url}
              onUebernehmen={uebernehmen}
              onAlle={alleUebernehmen}
              onSchliessen={() => setOffDaten(null)}
            />
          )}
          {!isObject && (<>
          <div className="row">
            <label className="grow">
              Einheit
              <select value={form.unit_id} onChange={(e) => set("unit_id", e.target.value)} disabled={readOnly} required>
                <option value="">– wählen –</option>
                {KIND_ORDER.filter((k) => units.some((u) => u.kind === k)).map((k) => (
                  <optgroup key={k} label={KIND_LABEL[k]}>
                    {units.filter((u) => u.kind === k).map((u) => (
                      <option key={u.id} value={u.id}>{u.name}</option>
                    ))}
                  </optgroup>
                ))}
              </select>
            </label>
            <label className="grow">
              Packungsgröße{baseShort ? ` (in ${baseShort})` : ""}
              <input type="number" step="any" value={form.package_size}
                onChange={(e) => set("package_size", e.target.value)} disabled={readOnly} placeholder="z.B. 500" />
            </label>
          </div>
          <div className="row">
            <label className="grow">
              <span className="tip" title="Verwaltet unter Verwaltung → Gebinde, dort auch mit Mehrzahl.">
                Gebinde
              </span>
              <select value={form.package_label} onChange={(e) => set("package_label", e.target.value)}
                disabled={readOnly}>
                <option value="">Packung (Standard)</option>
                {gebindearten.map((g) => (
                  <option key={g.id} value={g.singular}>{g.singular} / {g.plural}</option>
                ))}
                {/* Ein Artikel kann eine Bezeichnung tragen, die es in der
                    Verwaltung (noch) nicht gibt – etwa aus einem Import. */}
                {form.package_label && !gebindearten.some((g) => g.singular === form.package_label) && (
                  <option value={form.package_label}>{form.package_label}</option>
                )}
              </select>
            </label>
            <label className="grow">
              <span className="tip" title="Voreinstellung beim Einlagern – z.B. Konserven tragen oft nur „09/2026“.">
                MHD-Angabe
              </span>
              <select value={form.date_precision} onChange={(e) => set("date_precision", e.target.value)}
                disabled={readOnly}>
                <option value="day">Tagesdatum</option>
                <option value="month">nur Monat/Jahr</option>
              </select>
            </label>
          </div>
          <div className="row">
            <label className="grow">
              Mindestbestand
              <div className="field-inline">
                <input type="number" step="any" value={form.min_stock}
                  onChange={(e) => set("min_stock", e.target.value)} disabled={readOnly} placeholder="z.B. 2" />
                <select value={minUnit || form.unit_id} onChange={(e) => changeMinUnit(e.target.value)}
                  disabled={readOnly} style={{ marginTop: 0 }}>
                  {units
                    .filter((u) => !selectedUnit || u.kind === selectedUnit.kind)
                    .map((u) => <option key={u.id} value={u.id}>{u.name}</option>)}
                  {form.package_size && (
                    <option value="package">
                      {form.package_label.trim() || "Packung"} à {fmt(form.package_size)} {baseShort}
                    </option>
                  )}
                </select>
              </div>
            </label>
            <label className="grow">
              <span className="tip" title="Zählt Bestände mehrerer Marken zusammen. Der EAN-Code dieses Artikels erscheint danach automatisch bei der Gruppe.">
                Gruppe
              </span>
              <select value={form.group_id} onChange={(e) => set("group_id", e.target.value)} disabled={readOnly}>
                <option value="">– keine –</option>
                {groups.map((g) => <option key={g.id} value={g.id}>{g.name}</option>)}
              </select>
            </label>
          </div>

          {!isNew && product && isAdmin && (
            <div style={{ marginTop: "var(--sp-2)" }}>
              <div className="card-head" style={{ marginBottom: "var(--sp-1)" }}>
                <Icon name="location" size={16} />
                <h3 style={{ margin: 0 }}>Mindestbestand je Lagerort</h3>
              </div>
              <p className="muted small mt-0">
                Zusätzlich zum Gesamt-Mindestbestand: eigener Bedarf je Lagerort (z.B.
                Ferienhaus, Zuhause). Menge in {product.package_label || product.unit_name || "Stück"}.
                Wird separat gespeichert.
              </p>
              <LocationMinStock
                locations={locations}
                initial={product.location_min_stocks || []}
                unitLabel={product.package_label || product.unit_name || "Stück"}
                onError={(m) => toast(m, "warn")}
                onSave={async (list) => {
                  const updated = await api.setProductLocationMinStock(product.id, list);
                  setProduct(updated);
                  toast("Bedarfe je Lagerort gespeichert.");
                }}
              />
            </div>
          )}
          </>)}
          <div className="row">
            <label className="grow seg-label">
              Typ
              <div className="segmented">
                <button type="button" className={modus === "food" ? "active" : ""}
                  onClick={() => changeModus("food")} disabled={readOnly}>
                  Lebensmittel
                </button>
                <button type="button" className={modus === "object" ? "active" : ""}
                  onClick={() => changeModus("object")} disabled={readOnly}>
                  Gegenstand
                </button>
              </div>
            </label>
            <div className="grow" />
          </div>
          <div className="row">
            <label className="grow">
              <span className="tip" title="Nur Kategorien des gewählten Typs. Gegenstands-Kategorien zeigen Menge je Lagerort, Lebensmittel MHD/Chargen.">
                Kategorie
              </span>
              <CategorySelect
                value={form.category_id === "" ? null : Number(form.category_id)}
                nodes={asTree(categories).filter((c) => c.tracking === modus)}
                rootLabel="– keine –"
                disabled={readOnly}
                onChange={(id) => set("category_id", id == null ? "" : String(id))}
              />
            </label>
            {isAdmin && (
              <div className="grow" style={{ display: "flex", alignItems: "flex-end" }}>
                <button type="button" className="btn" style={{ marginBottom: 2 }}
                  onClick={() => (newCat ? setNewCat(null) : openNewCat())}>
                  <Icon name="plus" size={16} />Neue Kategorie
                </button>
              </div>
            )}
          </div>

          {isAdmin && newCat && (
            <div className="card-sub" style={{ marginBottom: "var(--sp-3)" }}>
              <div className="card-head"><Icon name="tag" />
                <h3>Neue {modus === "object" ? "Gegenstands" : "Lebensmittel"}-Kategorie</h3>
                <button type="button" className="btn-icon" style={{ marginLeft: "auto" }}
                  onClick={() => setNewCat(null)}><Icon name="close" size={16} /></button>
              </div>
              <div className="row">
                <label className="grow">Name
                  <input value={newCat.name} placeholder="z.B. Powerbank"
                    onChange={(e) => setNewCat({ ...newCat, name: e.target.value })} />
                </label>
                <label className="grow">Untergeordnet (optional)
                  <CategorySelect
                    value={newCat.parent_id === "" ? null : Number(newCat.parent_id)}
                    nodes={asTree(categories).filter((c) => c.tracking === modus)}
                    rootLabel="– oberste Ebene –"
                    onChange={(id) => setNewCat({ ...newCat, parent_id: id == null ? "" : String(id) })}
                  />
                </label>
              </div>
              <button type="button" className="btn primary" onClick={createCategoryInline}>
                Anlegen &amp; auswählen
              </button>
            </div>
          )}

          {isObject && (<>
            <div className="row">
              <label className="grow seg-label">
                Verwaltung
                <div className="segmented">
                  <button type="button" className={!form.individual ? "active" : ""}
                    onClick={() => set("individual", false)} disabled={readOnly}>Menge je Lagerort</button>
                  <button type="button" className={form.individual ? "active" : ""}
                    onClick={() => set("individual", true)} disabled={readOnly}>Einzelstücke</button>
                </div>
              </label>
              <div className="grow" />
            </div>
            <div className="row">
              {!form.individual && (
                <label className="grow">
                  Gekauft bei
                  <select value={form.shop_id} onChange={(e) => set("shop_id", e.target.value)}
                    disabled={readOnly}>
                    <option value="">– unbekannt / mehrere –</option>
                    {shops.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
                  </select>
                </label>
              )}
              <label className="grow">
                Produktlink
                <div className="field-inline">
                  <input type="url" value={form.product_url} placeholder="https://…"
                    onChange={(e) => set("product_url", e.target.value)} disabled={readOnly} />
                  {form.product_url && (
                    <a className="btn" href={form.product_url} target="_blank" rel="noreferrer"
                      title="Im Onlineshop öffnen">Öffnen</a>
                  )}
                </div>
              </label>
            </div>
            {form.category_id !== "" && (
              <div style={{ marginTop: "var(--sp-2)" }}>
                <div className="card-head" style={{ marginBottom: "var(--sp-2)" }}>
                  <Icon name="tag" /><h2>Eigene Felder</h2>
                  {isAdmin && (
                    <button type="button" className="btn ghost" style={{ marginLeft: "auto" }}
                      onClick={() => (newField ? setNewField(null)
                        : setNewField({ label: "", field_type: "text", unit: "", options: "", required: false }))}>
                      <Icon name="plus" size={16} />Feld
                    </button>
                  )}
                </div>
                {effFields.length > 0 ? (
                  <DynamicFields fields={effFields} values={fieldValues}
                    onChange={setField} disabled={readOnly} />
                ) : (
                  <p className="muted small">Für diese Kategorie sind noch keine Felder festgelegt.</p>
                )}
                {isAdmin && newField && (
                  <div className="card-sub" style={{ marginTop: "var(--sp-2)" }}>
                    <div className="row">
                      <label className="grow">Feldname
                        <input value={newField.label} placeholder="z.B. Kapazität"
                          onChange={(e) => setNewField({ ...newField, label: e.target.value })} />
                      </label>
                      <label style={{ width: 190 }}>Typ
                        <select value={newField.field_type}
                          onChange={(e) => setNewField({ ...newField, field_type: e.target.value })}>
                          {FIELD_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
                        </select>
                      </label>
                    </div>
                    {newField.field_type === "number" && (
                      <label>Einheit (optional)
                        <input value={newField.unit} placeholder="z.B. mAh"
                          onChange={(e) => setNewField({ ...newField, unit: e.target.value })} />
                      </label>
                    )}
                    {newField.field_type === "select" && (
                      <label>Auswahlmöglichkeiten (mit Komma trennen)
                        <input value={newField.options} placeholder="z.B. S, M, L, XL"
                          onChange={(e) => setNewField({ ...newField, options: e.target.value })} />
                      </label>
                    )}
                    <label className="check-inline">
                      <input type="checkbox" checked={newField.required}
                        onChange={(e) => setNewField({ ...newField, required: e.target.checked })} />
                      <span>Pflichtfeld</span>
                    </label>
                    <button type="button" className="btn primary" onClick={createFieldInline}>
                      Feld hinzufügen
                    </button>
                  </div>
                )}
              </div>
            )}
          </>)}

          {isAdmin && (
            <div className="field-inline" style={{ marginTop: "var(--sp-2)" }}>
              <button className="btn primary" disabled={busy}>{busy ? "Speichern…" : "Speichern"}</button>
              {!isNew && (
                <button type="button" className="btn danger" onClick={remove}>
                  <Icon name="trash" size={16} />Löschen
                </button>
              )}
            </div>
          )}
          {readOnly && <p className="muted small mt-0">Nur Administratoren können Produkte bearbeiten.</p>}
        </form>

        {!isNew && (isObject ? (
          product && !form.individual && (
            <ObjektBestand
              product={product}
              lots={lots}
              locations={locations}
              isAdmin={isAdmin}
              onChanged={async () => {
                setLots(await api.listLots(id));
                setProduct(await api.getProduct(id));
              }}
              onError={setError}
            />
          )
        ) : (
          <LotsCard
            product={product}
            lots={lots}
            baseShort={baseShort}
            warnDays={warnDays}
            isAdmin={isAdmin}
            onChanged={async () => {
              setLots(await api.listLots(id));
              setProduct(await api.getProduct(id));
            }}
            onError={setError}
          />
        ))}
      </div>

      {/* Einzelstücke über die volle Breite – die Tabelle hat viele Spalten. */}
      {!isNew && product && isObject && form.individual && (
        <Einzelstuecke
          product={product}
          locations={locations}
          shops={shops}
          isAdmin={isAdmin}
          onChanged={async () => { setProduct(await api.getProduct(id)); }}
          onError={setError}
        />
      )}

      {/* Unterhalb der beiden Spalten: die Codes braucht man selten, sie sollen
          Formular und Chargen nicht auseinanderschieben. */}
      {!isNew && product && (
        <section className="card">
          <div className="card-head"><Icon name="search" /><h2>Weitere EAN-Codes</h2></div>
          <BarcodeList
            barcodes={product.barcodes || []}
            disabled={readOnly}
            hint="Nützlich, wenn dasselbe Produkt mehrere Codes hat (z.B. Aktionspackung)."
            onAdd={async (body) => {
              try {
                setProduct(await api.addProductBarcode(id, body));
              } catch (err) { setError(err.message); }
            }}
            onDelete={async (code) => {
              try {
                await api.deleteProductBarcode(id, code);
                setProduct(await api.getProduct(id));
              } catch (err) { setError(err.message); }
            }}
          />
        </section>
      )}
    </div>
  );
}

/** Chargen-Karte mit Bearbeiten/Löschen. Bearbeitet wird in der Produkteinheit. */
function LotsCard({ product, lots, baseShort, warnDays, isAdmin, onChanged, onError }) {
  const confirm = useConfirm();
  const toast = useToast();
  const { formatBestBefore } = useSettings();
  const [editId, setEditId] = useState(null);
  const [draft, setDraft] = useState({ quantity: "", best_before: "", precision: "day" });
  // In welcher Einheit die Menge bearbeitet wird: "unit" oder "package".
  const [editUnit, setEditUnit] = useState("unit");

  const unitFactor = product?.unit_factor || 1;
  const unitName = product?.unit_name || baseShort;
  const pkgSize = product?.package_size || 0;
  const pkgLabel = product?.package_label || "Packung";

  // Leitangabe: Packungen, sobald eine Packungsgröße hinterlegt ist –
  // sonst die Produkteinheit. Darunter steht die jeweils andere Angabe klein.
  const primaryFactor = pkgSize > 0 ? pkgSize : unitFactor;
  // Mengenabhaengig: "1 Glas", aber "3 Gläser". Einheiten (Gramm, Liter)
  // bleiben unveraendert - die haben im Deutschen keine Mehrzahl.
  const primaryLabel = (menge) => (pkgSize > 0 ? gebinde(menge, pkgLabel) : unitName);

  function secondary(qty) {
    if (pkgSize > 0) return `${fmt(qty / unitFactor)} ${unitName}`;
    return unitFactor !== 1 ? `${fmt(qty)} ${baseShort}` : "";
  }

  // Faktor der aktuell im Editor gewählten Eingabeeinheit.
  function editFactorOf(mode) {
    return mode === "package" && pkgSize > 0 ? pkgSize : unitFactor;
  }
  const editFactor = editFactorOf(editUnit);

  function startEdit(l) {
    // Bearbeiten startet in der Leitangabe (Packungen, falls vorhanden).
    const mode = pkgSize > 0 ? "package" : "unit";
    setEditId(l.id);
    setEditUnit(mode);
    setDraft({
      quantity: l.quantity / editFactorOf(mode),
      best_before:
        l.best_before_precision === "month"
          ? toMonthInput(l.best_before)
          : l.best_before || "",
      precision: l.best_before_precision === "month" ? "month" : "day",
    });
  }

  function changeEditUnit(newMode) {
    if (draft.quantity !== "" && newMode !== editUnit) {
      const oldF = editFactorOf(editUnit);
      const newF = editFactorOf(newMode);
      setDraft((d) => ({ ...d, quantity: String((Number(d.quantity) * oldF) / newF) }));
    }
    setEditUnit(newMode);
  }

  async function saveEdit(l) {
    try {
      await api.updateLot(l.id, {
        quantity: Number(draft.quantity) * editFactor,
        best_before:
          (draft.precision === "month" ? fromMonthInput(draft.best_before) : draft.best_before) ||
          null,
        best_before_precision: draft.precision || "day",
      });
      setEditId(null);
      await onChanged();
    } catch (err) {
      onError(err.message);
    }
  }

  async function removeLot(l) {
    const ok = await confirm({
      title: "Charge löschen?",
      message: "Die Menge wird als Korrektur im Verlauf protokolliert.",
      confirmLabel: "Löschen",
      danger: true,
    });
    if (!ok) return;
    try {
      await api.deleteLot(l.id);
      await onChanged();
    } catch (err) {
      onError(err.message);
    }
  }

  return (
    <section className="card">
      <div className="card-head"><Icon name="box" /><h2>Chargen im Bestand</h2></div>
      {lots.some((l) => isExpired(l.best_before)) && (
        <div className="alert error"><Icon name="alert" size={16} />
          Dieses Produkt hat abgelaufene Chargen im Bestand.
        </div>
      )}
      <div className="table-wrap">
        <table className="table">
          <thead>
            <tr>
              <th className="num" style={{ minWidth: 150 }}>Menge</th>
              <th style={{ minWidth: 160 }}>MHD</th>
              <th>Eingelagert</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            {lots.map((l) => {
              const cls = expiryRowClass(l.best_before, warnDays);
              const dLeft = daysUntil(l.best_before);
              const editing = editId === l.id;
              return (
                <tr key={l.id} className={cls}>
                  <td data-label="Menge" className="num">
                    {editing ? (
                      <div className="field-inline" style={{ justifyContent: "flex-end" }}>
                        <input type="number" step="any" min="0" value={draft.quantity}
                          onChange={(e) => setDraft({ ...draft, quantity: e.target.value })}
                          style={{ marginTop: 0 }} />
                        {pkgSize > 0 ? (
                          <select value={editUnit} onChange={(e) => changeEditUnit(e.target.value)}
                            style={{ marginTop: 0 }}>
                            <option value="package">{pkgLabel}</option>
                            <option value="unit">{unitName}</option>
                          </select>
                        ) : (
                          <span className="muted small">{unitName}</span>
                        )}
                      </div>
                    ) : (
                      <>
                        {fmt(l.quantity / primaryFactor)} {primaryLabel(l.quantity / primaryFactor)}
                        {secondary(l.quantity) && (
                          <div className="muted small">{secondary(l.quantity)}</div>
                        )}
                      </>
                    )}
                  </td>
                  <td data-label="MHD">
                    {editing ? (
                      <input type={draft.precision === "month" ? "month" : "date"}
                        value={draft.best_before}
                        onChange={(e) => setDraft({ ...draft, best_before: e.target.value })}
                        style={{ marginTop: 0, minWidth: 150 }} />
                    ) : (
                      <>
                        {l.best_before ? formatBestBefore(l.best_before, l.best_before_precision) : "–"}
                        {cls && (
                          <span className={`badge ${cls === "row-danger" ? "danger" : "warn"}`}
                            style={{ marginLeft: 6 }}>
                            {relativeExpiry(dLeft)}
                          </span>
                        )}
                      </>
                    )}
                  </td>
                  <td data-label="Eingelagert" className="muted">{new Date(l.created_at).toLocaleDateString("de-DE")}</td>
                  <td className="num">
                    {isAdmin && (editing ? (
                      <div className="btn-pair">
                        <button className="btn sm primary" onClick={() => saveEdit(l)}>Speichern</button>
                        <button className="btn sm" onClick={() => setEditId(null)}>Abbrechen</button>
                      </div>
                    ) : (
                      <div className="field-inline" style={{ justifyContent: "flex-end" }}>
                        <button className="btn-icon" onClick={() => startEdit(l)} title="Charge bearbeiten">
                          <Icon name="edit" size={16} />
                        </button>
                        <button className="btn-icon danger" onClick={() => removeLot(l)} title="Charge löschen">
                          <Icon name="trash" size={16} />
                        </button>
                      </div>
                    ))}
                  </td>
                </tr>
              );
            })}
            {lots.length === 0 && <tr><td colSpan={4} className="empty">Keine Chargen im Bestand.</td></tr>}
          </tbody>
        </table>
      </div>
      {pkgSize > 0 && (
        <p className="muted small">1 {pkgLabel} = {fmt(pkgSize)} {baseShort}</p>
      )}
    </section>
  );
}
