// Тест-панель імпорту товарів у каталог Horoshop (catalog/import)
// Desktop only. Тільки для owner. Дозволяє вручну пробувати оновлення/додавання
// товарів з максимумом параметрів — через форму або сирий JSON.
const { useState: useStateTP, useMemo: useMemoTP, useEffect: useEffectTP } = React;

// ── Коди журналу імпорту (для розмальовки відповіді) ───────────────
const TP_OK_CODES   = new Set([0, 3, 4, 9, 15, 22, 28]); // успіх / інформація
const TP_WARN_CODES = new Set([13, 16]);                  // попередження
// решта → помилка (червоний)
function tpCodeColor(code) {
  if (TP_OK_CODES.has(code))   return "var(--credit, #10B981)";
  if (TP_WARN_CODES.has(code)) return "var(--warning, #F59E0B)";
  return "var(--danger, #F43F5E)";
}

// ── Дрібні UI-хелпери ──────────────────────────────────────────────
function TPLabel({ children, hint }) {
  return (
    <label style={{ display: "block", fontSize: 12, fontWeight: 500, color: "var(--fg-secondary)", marginBottom: 5 }}>
      {children}
      {hint && <span style={{ color: "var(--fg-muted)", fontWeight: 400, marginLeft: 6 }}>{hint}</span>}
    </label>
  );
}

const tpInputStyle = {
  width: "100%", boxSizing: "border-box", height: 36, padding: "0 10px",
  background: "var(--bg-base)", border: "1px solid var(--border-default)", borderRadius: 8,
  color: "var(--fg-primary)", fontSize: 13, fontFamily: "inherit", outline: "none",
};

function TPInput({ value, onChange, placeholder, type = "text", list }) {
  return (
    <input type={type} value={value ?? ""} placeholder={placeholder} list={list}
      onChange={e => onChange(e.target.value)} style={tpInputStyle}/>
  );
}

function TPTextarea({ value, onChange, placeholder, rows = 3, mono }) {
  return (
    <textarea value={value ?? ""} placeholder={placeholder} rows={rows}
      onChange={e => onChange(e.target.value)}
      style={{ ...tpInputStyle, height: "auto", padding: "8px 10px", resize: "vertical",
        fontFamily: mono ? "var(--font-mono, ui-monospace, monospace)" : "inherit", lineHeight: 1.5 }}/>
  );
}

function TPToggle({ value, onChange, label }) {
  return (
    <button type="button" onClick={() => onChange(!value)} style={{
      display: "inline-flex", alignItems: "center", gap: 8, background: "transparent",
      border: 0, cursor: "pointer", fontFamily: "inherit", padding: 0,
    }}>
      <span style={{
        width: 38, height: 22, borderRadius: 999, flexShrink: 0, position: "relative",
        background: value ? "var(--accent)" : "var(--border-strong, #3a3f4b)", transition: "background 150ms",
      }}>
        <span style={{
          position: "absolute", top: 2, left: value ? 18 : 2, width: 18, height: 18, borderRadius: "50%",
          background: "#fff", transition: "left 150ms",
        }}/>
      </span>
      <span style={{ fontSize: 13, color: "var(--fg-primary)" }}>{label}</span>
    </button>
  );
}

function TPSection({ title, children, defaultOpen = true }) {
  const [open, setOpen] = useStateTP(defaultOpen);
  return (
    <div style={{ border: "1px solid var(--border-subtle)", borderRadius: 12, background: "var(--bg-panel)", marginBottom: 14, overflow: "hidden" }}>
      <button type="button" onClick={() => setOpen(o => !o)} style={{
        width: "100%", display: "flex", alignItems: "center", justifyContent: "space-between",
        padding: "12px 16px", background: "transparent", border: 0, cursor: "pointer", fontFamily: "inherit",
      }}>
        <span style={{ fontSize: 13, fontWeight: 600, color: "var(--fg-primary)" }}>{title}</span>
        <Icon name={open ? "chevron-up" : "chevron-down"} size={16} style={{ color: "var(--fg-muted)" }}/>
      </button>
      {open && <div style={{ padding: "0 16px 16px" }}>{children}</div>}
    </div>
  );
}

// Сітка 2 колонки
function TPGrid({ children, cols = 2 }) {
  return <div style={{ display: "grid", gridTemplateColumns: `repeat(${cols}, 1fr)`, gap: 12 }}>{children}</div>;
}

// Двомовне поле (ua / ru)
function TPBiField({ label, hint, val, onChange, textarea, rows }) {
  const v = val || {};
  const set = (lang, x) => onChange({ ...v, [lang]: x });
  const Comp = textarea ? TPTextarea : TPInput;
  return (
    <div>
      <TPLabel hint={hint}>{label}</TPLabel>
      <TPGrid>
        <Comp value={v.ua} onChange={x => set("ua", x)} placeholder="🇺🇦 укр" rows={rows}/>
        <Comp value={v.ru} onChange={x => set("ru", x)} placeholder="🇷🇺 рус" rows={rows}/>
      </TPGrid>
    </div>
  );
}

// ── Початковий стан форми ──────────────────────────────────────────
// Значення дропдаунів з сайту Horoshop (Сергій надав 2026-06-05)
const TP_PRESENCE       = ["В наявності", "Очікується", "Під замовлення 2-3 дні", "Під замовлення 3-7 днів", "Під замовлення 7-14 днів", "Немає в наявності"];
const TP_CONDITION      = ["Новий", "Відновлений", "б/в"];
const TP_GUARANTEE_SHOP = ["Магазин", "Виробник"];
const TP_MARKETPLACES   = ["Google Feed for Merchant Center", "Hotline Feed", "Rozetka Feed"];
const TP_ICONS          = ["Новинка", "Хіт", "Розпродаж"];

function tpEmptyForm() {
  return {
    article: "", parent_article: "", article_for_display: "",
    title: {}, mod_title: {}, brand: "",
    price: "", price_old: "", discount: "", currency: "UAH",
    presence: "В наявності", display_in_showcase: true,
    supplierPrefix: "",   // постачальник-джерело (cbPrefix) → сервер додасть supplier:{id}
    parent: "", parent_id: "", alt_parent: "",
    short_description: {}, description: {},
    seo_title: "", seo_keywords: "", seo_description: "", h1_title: {},
    color: "", gtin: "", mpn: "", popularity: "",
    guarantee_shop: "Магазин", guarantee_length: "", uktzed: "", condition: "Новий",
    icons: "", export_to_marketplace: [...TP_MARKETPLACES],
    chars: [],
    img_links: "", img_override: true, img_removeAll: false,
  };
}

// Прибрати порожні значення з обʼєкта (рекурсивно для {ua,ru})
function tpClean(obj) {
  const out = {};
  for (const [k, v] of Object.entries(obj)) {
    if (v == null || v === "") continue;
    if (typeof v === "object" && !Array.isArray(v)) {
      const inner = tpClean(v);
      if (Object.keys(inner).length) out[k] = inner;
    } else {
      out[k] = v;
    }
  }
  return out;
}

// Форма → обʼєкт продукту для API
function tpBuildProduct(f) {
  const p = {};
  if (f.article) p.article = f.article.trim();
  if (f.parent_article) p.parent_article = f.parent_article.trim();
  if (f.article_for_display) p.article_for_display = f.article_for_display.trim();

  const title = tpClean(f.title || {});
  if (Object.keys(title).length) p.title = title;
  const modTitle = tpClean(f.mod_title || {});
  if (Object.keys(modTitle).length) p.mod_title = modTitle;
  if (f.brand) p.brand = f.brand;

  if (f.price !== "") p.price = Number(f.price);
  if (f.price_old !== "") p.price_old = Number(f.price_old);
  if (f.discount !== "") p.discount = Number(f.discount);
  if (f.currency) p.currency = f.currency;
  if (f.presence) p.presence = f.presence;
  if (f.supplierPrefix) p.sourceSup = f.supplierPrefix;   // сервер /api/horoshop/import → supplier:{id}
  p.display_in_showcase = !!f.display_in_showcase;

  // Категорія: id має пріоритет над шляхом
  if (f.parent_id) p.parent = { id: Number(f.parent_id) };
  else if (f.parent) p.parent = f.parent;
  const alt = (f.alt_parent || "").split("\n").map(s => s.trim()).filter(Boolean)
    .map(s => /^\d+$/.test(s) ? { id: Number(s) } : s);
  if (alt.length) p.alt_parent = alt;

  const sd = tpClean(f.short_description || {});
  if (Object.keys(sd).length) p.short_description = sd;
  const desc = tpClean(f.description || {});
  if (Object.keys(desc).length) p.description = desc;

  if (f.seo_title) p.seo_title = f.seo_title;
  if (f.seo_keywords) p.seo_keywords = f.seo_keywords;
  if (f.seo_description) p.seo_description = f.seo_description;
  const h1 = tpClean(f.h1_title || {});
  if (Object.keys(h1).length) p.h1_title = h1;

  if (f.color) p.color = f.color;
  if (f.gtin) p.gtin = f.gtin;
  if (f.mpn) p.mpn = f.mpn;
  if (f.popularity !== "") p.popularity = Number(f.popularity);
  if (f.guarantee_shop) p.guarantee_shop = f.guarantee_shop;
  if (f.guarantee_length !== "") p.guarantee_length = Number(f.guarantee_length);
  if (f.uktzed) p.uktzed = f.uktzed;
  if (f.condition) p.condition = f.condition;

  const icons = (f.icons || "").split(",").map(s => s.trim()).filter(Boolean);
  if (icons.length) p.icons = icons;

  // Маркетплейси — назви через крапку з комою
  if (Array.isArray(f.export_to_marketplace) && f.export_to_marketplace.length) {
    p.export_to_marketplace = f.export_to_marketplace.join(";");
  }

  const ch = {};
  for (const c of (f.chars || [])) {
    const key = (c.key || "").trim();
    if (!key) continue;
    const ua = (c.ua || "").trim();
    const ru = (c.ru || "").trim();
    if (!ua && !ru) continue;
    ch[key] = (ua && ru) ? { ua, ru } : (ua || ru);
  }
  if (Object.keys(ch).length) p.characteristics = ch;

  const links = (f.img_links || "").split("\n").map(s => s.trim()).filter(Boolean);
  if (links.length || f.img_removeAll) {
    p.images = {};
    if (f.img_removeAll) {
      p.images.removeAll = true;
    } else {
      p.images.links = links;
      p.images.override = !!f.img_override;
    }
  }

  return p;
}

// ── Префіл форми з сирого обʼєкта товару (catalog/export) ──────────
// Двомовне значення з різних можливих форматів → {ua, ru}
function tpLang(v) {
  if (v == null) return null;
  if (typeof v === "string") return { ua: v, ru: v };
  if (typeof v === "object") {
    const ua = v.ua ?? v.uk ?? v.ua_UA ?? "";
    const ru = v.ru ?? v.ru_RU ?? "";
    if (ua === "" && ru === "") return null;
    return { ua: String(ua || ""), ru: String(ru || "") };
  }
  return null;
}
// Перше непорожнє значення серед кількох можливих ключів
function tpPick(obj, keys) {
  for (const k of keys) {
    if (obj[k] !== undefined && obj[k] !== null && obj[k] !== "") return obj[k];
  }
  return undefined;
}
// Зібрати масив URL зображень із різних форматів галереї
function tpImages(raw) {
  const sources = [raw.images, raw.gallery, raw.gallery_common, raw.photos, raw.image];
  const urls = [];
  for (const src of sources) {
    if (!src) continue;
    const arr = Array.isArray(src) ? src : (Array.isArray(src.links) ? src.links : [src]);
    for (const item of arr) {
      if (!item) continue;
      if (typeof item === "string") urls.push(item);
      else urls.push(item.url || item.src || item.link || item.href || "");
    }
  }
  return [...new Set(urls.filter(Boolean))];
}
// Розгорнути обгортку Horoshop {id, value} → саме значення (рядок / null / {ua,ru})
function tpUnwrap(v) {
  if (v && typeof v === "object" && !Array.isArray(v) && "value" in v && "id" in v) {
    return v.value;
  }
  return v;
}
// Будь-яке поле → один рядок (укр пріоритет), з урахуванням обгортки {id,value}
function tpStr(v) {
  v = tpUnwrap(v);
  if (v == null) return "";
  if (typeof v === "string") return v;
  if (Array.isArray(v)) return v.join("; ");
  const l = tpLang(v);
  return l ? l.ua : "";
}
function tpPrefill(raw, fallbackArt) {
  const upd = {};
  upd.article = raw.article || fallbackArt;
  if (raw.parent_article) upd.parent_article = raw.parent_article;
  if (raw.article_for_display) upd.article_for_display = raw.article_for_display;

  const title = tpLang(tpUnwrap(tpPick(raw, ["title", "name"])));            if (title) upd.title = title;
  const modT  = tpLang(tpUnwrap(raw.mod_title));                             if (modT)  upd.mod_title = modT;
  const sd    = tpLang(tpUnwrap(tpPick(raw, ["short_description", "short_desc"]))); if (sd) upd.short_description = sd;
  const desc  = tpLang(tpUnwrap(tpPick(raw, ["description", "desc", "text"])));      if (desc) upd.description = desc;

  const brand = tpStr(tpPick(raw, ["brand", "vendor", "producer"])); if (brand) upd.brand = brand;
  if (raw.price != null && raw.price !== "") upd.price = String(raw.price);
  const oldP = tpPick(raw, ["price_old", "old_price"]); if (oldP != null && oldP !== "") upd.price_old = String(oldP);
  if (raw.discount != null && raw.discount !== "") upd.discount = String(raw.discount);
  const curr = tpStr(raw.currency); if (curr) upd.currency = curr;
  const pres = tpStr(raw.presence); if (pres) upd.presence = pres;
  if (raw.display_in_showcase != null) upd.display_in_showcase = !!(raw.display_in_showcase === true || raw.display_in_showcase === 1 || raw.display_in_showcase === "1");

  const color = tpStr(tpPick(raw, ["color", "colour"])); if (color) upd.color = color;
  if (raw.gtin) upd.gtin = raw.gtin;
  if (raw.mpn) upd.mpn = raw.mpn;
  if (raw.popularity != null && raw.popularity !== "") upd.popularity = String(raw.popularity);
  const gShop = tpStr(raw.guarantee_shop); if (gShop) upd.guarantee_shop = gShop;
  if (raw.guarantee_length != null && raw.guarantee_length !== "") upd.guarantee_length = String(raw.guarantee_length);
  if (raw.uktzed) upd.uktzed = raw.uktzed;
  const cond = tpStr(tpPick(raw, ["condition", "state"])); if (cond) upd.condition = cond;

  const seoT = tpLang(tpUnwrap(raw.seo_title));       if (seoT) upd.seo_title = seoT.ua;
  const seoK = tpLang(tpUnwrap(raw.seo_keywords));    if (seoK) upd.seo_keywords = seoK.ua;
  const seoD = tpLang(tpUnwrap(raw.seo_description)); if (seoD) upd.seo_description = seoD.ua;

  const icons = raw.icons;
  if (Array.isArray(icons) && icons.length) upd.icons = icons.map(tpStr).filter(Boolean).join(", ");

  // Категорія: {id, value:"шлях"} → беремо id (пріоритет) і шлях
  const parent = tpPick(raw, ["parent", "category", "parent_id", "category_id"]);
  if (parent != null) {
    if (typeof parent === "object" && parent.id != null) upd.parent_id = String(parent.id);
    else if (/^\d+$/.test(String(parent))) upd.parent_id = String(parent);
    else upd.parent = tpStr(parent);
  }

  // Характеристики → динамічний список {key, ua, ru}, зберігаємо ключі як на сайті
  const rawCh = raw.characteristics || raw.attributes || raw.params;
  const chars = [];
  if (rawCh && typeof rawCh === "object" && !Array.isArray(rawCh)) {
    for (const [k, v] of Object.entries(rawCh)) {
      if (!k) continue;
      const lv = tpLang(tpUnwrap(v));
      const ua = lv ? lv.ua : (typeof v === "string" ? v : "");
      const ru = lv ? lv.ru : "";
      if (!ua && !ru) continue;                 // порожні характеристики не тягнемо
      chars.push({ key: k, ua, ru });
    }
  } else if (Array.isArray(rawCh)) {
    for (const it of rawCh) {
      const k = it.name || it.title || it.key; if (!k) continue;
      const lv = tpLang(tpUnwrap(it.value ?? it.val ?? it.values));
      if (lv && (lv.ua || lv.ru)) chars.push({ key: k, ua: lv.ua, ru: lv.ru });
    }
  }
  if (chars.length) upd.chars = chars;

  // Зображення
  const imgs = tpImages(raw);
  if (imgs.length) { upd.img_links = imgs.join("\n"); upd.img_override = false; }

  return upd;
}

// Орієнтовна вартість виклику OpenAI ($/1M токенів [вхід, вихід])
const TP_OPENAI_PRICES = {
  "gpt-4o-mini": [0.15, 0.60], "gpt-4o": [2.5, 10], "gpt-4.1-mini": [0.4, 1.6],
  "gpt-4.1": [2, 8], "gpt-3.5-turbo": [0.5, 1.5], "gpt-4-turbo": [10, 30],
};
function tpAiCost(model, usage) {
  if (!usage) return null;
  const key = Object.keys(TP_OPENAI_PRICES).find(k => (model || "").startsWith(k));
  if (!key) return null;
  const [pin, pout] = TP_OPENAI_PRICES[key];
  const pt = usage.prompt_tokens || 0, ct = usage.completion_tokens || 0;
  return (pt / 1e6) * pin + (ct / 1e6) * pout;
}

// ── Вибір категорії з дерева Horoshop (pages/export) ───────────────
function TPCategoryPicker({ valueId, valuePath, onPick }) {
  const [cats, setCats]       = useStateTP([]);
  const [q, setQ]            = useStateTP("");
  const [open, setOpen]      = useStateTP(false);
  const [loading, setLoad]   = useStateTP(false);
  const [loaded, setLoaded]  = useStateTP(false);

  const load = async (force) => {
    setLoad(true);
    try {
      const r = await API.getHoroshopCategories(force);
      setCats(r.categories || []);
      setLoaded(true);
    } catch (e) { /* мовчки */ }
    finally { setLoad(false); }
  };

  const onFocus = () => { setOpen(true); if (!loaded && !loading) load(false); };
  const sel = cats.find(c => String(c.id) === String(valueId));
  const needle = q.trim().toLowerCase();
  const filtered = (needle ? cats.filter(c => c.path.toLowerCase().includes(needle)) : cats).slice(0, 80);

  return (
    <div style={{ position: "relative" }}>
      <TPLabel hint={loading ? "завантаження…" : "пошук по назві → обери зі списку"}>Обрати категорію з дерева</TPLabel>
      <div style={{ display: "flex", gap: 8 }}>
        <input
          value={q}
          placeholder={sel ? sel.path : (valuePath || "почни вводити назву категорії")}
          onFocus={onFocus}
          onChange={e => { setQ(e.target.value); setOpen(true); }}
          onBlur={() => setTimeout(() => setOpen(false), 150)}
          style={{ ...tpInputStyle, flex: 1 }}/>
        <button type="button" title="Оновити дерево" onClick={() => load(true)} style={{
          width: 36, height: 36, flexShrink: 0, border: "1px solid var(--border-default)", borderRadius: 8,
          background: "transparent", color: "var(--fg-muted)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center",
        }}><Icon name="refresh-cw" size={15}/></button>
      </div>
      {sel && <div style={{ fontSize: 11.5, color: "var(--credit)", marginTop: 4 }}>✓ {sel.path} <span style={{ color: "var(--fg-muted)" }}>#{sel.id}</span></div>}
      {open && (
        <div style={{
          position: "absolute", top: "100%", left: 0, right: 44, zIndex: 20, marginTop: 4,
          background: "var(--bg-panel)", border: "1px solid var(--border-default)", borderRadius: 8,
          maxHeight: 280, overflowY: "auto", boxShadow: "0 8px 24px rgba(0,0,0,.25)",
        }}>
          {loading && <div style={{ padding: 12, fontSize: 12.5, color: "var(--fg-muted)" }}>Завантаження категорій…</div>}
          {!loading && filtered.length === 0 && <div style={{ padding: 12, fontSize: 12.5, color: "var(--fg-muted)" }}>Нічого не знайдено</div>}
          {!loading && filtered.map(c => (
            <button key={c.id} type="button"
              onMouseDown={e => { e.preventDefault(); onPick(c.id, c.path); setOpen(false); setQ(""); }}
              style={{
                width: "100%", textAlign: "left", padding: "8px 12px", border: 0, background: "transparent",
                cursor: "pointer", fontFamily: "inherit", fontSize: 12.5, color: "var(--fg-primary)",
                display: "flex", justifyContent: "space-between", gap: 10, borderBottom: "1px solid var(--border-subtle)",
              }}
              onMouseEnter={e => e.currentTarget.style.background = "var(--bg-hover)"}
              onMouseLeave={e => e.currentTarget.style.background = "transparent"}>
              <span>{c.path}</span>
              <span style={{ color: "var(--fg-muted)", flexShrink: 0 }}>#{c.id}</span>
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

// ── Стан локального кешу каталогу + кнопка оновлення ─────────────────
function TPCatalogStatus() {
  const [st, setSt] = useStateTP(null);
  const [busy, setBusy] = useStateTP(false);
  const [err, setErr] = useStateTP(null);

  const load = async () => {
    try { setErr(null); setSt(await API.getCatalogStatus()); }
    catch (e) { setErr(e.message); }
  };
  useEffectTP(() => { load(); }, []);

  const refresh = async () => {
    setBusy(true); setErr(null);
    try { await API.refreshCatalog(); await load(); }
    catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  };

  const fmtDate = ts => ts ? new Date(ts).toLocaleString("uk-UA") : "—";
  const srcLabel = s => s === "cache" ? "локальний кеш" : s === "api" ? "API (свіже)" : "—";

  return (
    <div style={{ margin: "16px 0", padding: 12, background: "var(--bg-panel)", border: "1px solid var(--border-subtle)", borderRadius: 12 }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, flexWrap: "wrap" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
          <Icon name="database" size={15} color="var(--accent)"/>
          <span style={{ fontSize: 13, fontWeight: 600, color: "var(--fg-primary)" }}>Кеш каталогу</span>
        </div>
        <div style={{ display: "flex", gap: 6 }}>
          <Button variant="secondary" size="sm" leftIcon="refresh-cw" onClick={load} disabled={busy}>Статус</Button>
          <Button variant="primary" size="sm" leftIcon="download-cloud" onClick={refresh} disabled={busy}>
            {busy ? "Оновлюю…" : "Оновити каталог"}
          </Button>
        </div>
      </div>
      {err && <div style={{ fontSize: 12, color: "var(--danger)", marginTop: 8 }}>{err}</div>}
      {st && (
        <div style={{ fontSize: 12, color: "var(--fg-muted)", marginTop: 8, display: "flex", gap: 14, flexWrap: "wrap" }}>
          <span>Кеш-файл: <b style={{ color: st.hasCache ? "var(--credit)" : "var(--warning)" }}>{st.hasCache ? "є" : "немає"}</b></span>
          <span>Товарів у кеші: <b style={{ color: "var(--fg-secondary)" }}>{st.cacheCount ?? "—"}</b></span>
          <span>У памʼяті: <b style={{ color: "var(--fg-secondary)" }}>{st.count}</b></span>
          <span>Джерело: <b style={{ color: "var(--fg-secondary)" }}>{srcLabel(st.source)}</b></span>
          <span>Оновлено: <b style={{ color: "var(--fg-secondary)" }}>{fmtDate(st.lastUpdated)}</b></span>
        </div>
      )}
    </div>
  );
}

// ── Вибір AI-провайдера та моделі для генерації опису ───────────────
const TP_GROUP_ORDER = ["Recommended", "Cheap / Very cheap", "Balanced", "Quality", "Other", "Legacy / Deprecated"];
function tpModelGroup(m) {
  if (m.status === "deprecated") return "Legacy / Deprecated";
  if (m.recommended) return "Recommended";
  if (m.status === "legacy") return "Legacy / Deprecated";
  const uc = m.useCase || "";
  if (/cheap|nano/.test(uc)) return "Cheap / Very cheap";
  if (/balanced/.test(uc)) return "Balanced";
  if (/quality/.test(uc)) return "Quality";
  return "Other";
}
const tpPrice = n => (typeof n === "number") ? `$${n}` : "—";

// Орієнтовні токени на 1 генерацію товара (≈ за реальними прогонами ~20k вхідних сумарно).
// Вибір моделі змінює ЛИШЕ крок опису; факти+vision рахуються по дешевій fast-моделі (Haiku).
const TP_EST = {
  desc:   { in: 8000, out: 3000 },  // крок опису — саме його модель обираємо
  facts:  { in: 4000, out: 1500 },  // факти/назва/характеристики
  vision: { in: 8000, out: 2000 },  // класифікація фото
};
const tpStepCost = (pin, pout, t) =>
  (typeof pin === "number" && typeof pout === "number") ? (t.in / 1e6) * pin + (t.out / 1e6) * pout : null;
// Орієнтовна вартість 1 товара, якщо опис генерує модель m (факти/vision — на fastModel).
function tpProductCost(m, fastModel) {
  const desc = tpStepCost(m.inputPricePer1M, m.outputPricePer1M, TP_EST.desc);
  if (desc == null) return null;
  const fIn = fastModel?.inputPricePer1M ?? 1, fOut = fastModel?.outputPricePer1M ?? 5;
  const fixed = (tpStepCost(fIn, fOut, TP_EST.facts) || 0) + (tpStepCost(fIn, fOut, TP_EST.vision) || 0);
  return { desc, total: desc + fixed };
}
const tpFmtUsd = n => (n == null) ? "—" : (n < 0.01 ? `$${n.toFixed(4)}` : `$${n.toFixed(3)}`);

function TPModelPicker({ value, onChange, label, hint, imagesOnly }) {
  const [data, setData]   = useStateTP(null);     // { providers, envDefault }
  const [err, setErr]     = useStateTP(null);
  const [open, setOpen]   = useStateTP(false);
  const [prov, setProv]   = useStateTP(null);     // активний провайдер у списку
  const [fPhoto, setFPhoto] = useStateTP(false);
  const [fCheap, setFCheap] = useStateTP(false);
  const [fLegacy, setFLegacy] = useStateTP(false);
  const [liveBusy, setLiveBusy] = useStateTP(false);

  const load = async (live) => {
    try {
      setErr(null);
      if (live) setLiveBusy(true);
      const r = await API.getAiModels(live);
      setData(r);
      if (!prov) setProv(Object.keys(r.providers || {})[0] || "anthropic");
    } catch (e) { setErr(e.message); }
    finally { if (live) setLiveBusy(false); }
  };
  useEffectTP(() => { load(false); }, []);

  const providers = data?.providers || {};
  const provKeys = Object.keys(providers);
  const curProv = providers[prov];

  // Дешева fast-модель (Haiku) — для оцінки фіксованої частини (факти+vision) у вартості товара.
  const fastModel = useMemoTP(() => {
    const an = providers?.anthropic?.models || [];
    return an.find(m => m.id === providers?.anthropic?.defaultModel)
        || an.find(m => /haiku/.test(m.id))
        || { inputPricePer1M: 1, outputPricePer1M: 5 };
  }, [data]);

  // Поточна обрана модель (для підпису кнопки)
  let selLabel = "За замовчуванням (.env)";
  if (value?.model) {
    for (const p of Object.values(providers)) {
      const m = (p.models || []).find(x => x.id === value.model);
      if (m) { selLabel = `${p.label}: ${m.label}`; break; }
    }
    if (selLabel === "За замовчуванням (.env)") selLabel = value.model;
  }

  // Фільтрація + групування моделей активного провайдера
  const groups = useMemoTP(() => {
    if (!curProv) return [];
    let list = curProv.models || [];
    if (imagesOnly || fPhoto) list = list.filter(m => m.supportsImages);
    if (fCheap) list = list.filter(m => (typeof m.inputPricePer1M === "number" && m.inputPricePer1M <= 0.5) || /cheap|nano/.test(m.useCase || ""));
    if (!fLegacy) list = list.filter(m => m.status !== "legacy" && m.status !== "deprecated");
    const byGroup = {};
    for (const m of list) { const g = tpModelGroup(m); (byGroup[g] = byGroup[g] || []).push(m); }
    return TP_GROUP_ORDER.filter(g => byGroup[g]?.length).map(g => ({ group: g, items: byGroup[g] }));
  }, [curProv, fPhoto, fCheap, fLegacy, imagesOnly]);

  const pick = (provKey, m) => { onChange({ provider: provKey, model: m.id }); setOpen(false); };

  return (
    <div style={{ position: "relative" }}>
      <TPLabel hint={hint || "якою моделлю генерувати опис (факти/фото лишаються на дешевій)"}>{label || "AI-модель для опису"}</TPLabel>
      <div style={{ display: "flex", gap: 8 }}>
        <button type="button" onClick={() => setOpen(o => !o)} style={{
          ...tpInputStyle, flex: 1, textAlign: "left", cursor: "pointer", display: "flex",
          alignItems: "center", justifyContent: "space-between", gap: 8,
        }}>
          <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{selLabel}</span>
          <Icon name="chevron-down" size={15}/>
        </button>
        {value?.model && (
          <button type="button" title="Скинути на .env" onClick={() => onChange(null)} style={{
            width: 36, height: 36, flexShrink: 0, border: "1px solid var(--border-default)", borderRadius: 8,
            background: "transparent", color: "var(--fg-muted)", cursor: "pointer",
          }}><Icon name="x" size={15}/></button>
        )}
      </div>
      {err && <div style={{ fontSize: 11.5, color: "var(--danger)", marginTop: 4 }}>Моделі: {err}</div>}

      {open && (
        <div style={{
          position: "absolute", top: "100%", left: 0, right: 0, zIndex: 25, marginTop: 4,
          background: "var(--bg-panel)", border: "1px solid var(--border-default)", borderRadius: 10,
          boxShadow: "0 8px 28px rgba(0,0,0,.3)", padding: 10,
        }}>
          {/* Провайдери */}
          <div style={{ display: "flex", gap: 6, marginBottom: 8, flexWrap: "wrap" }}>
            {provKeys.map(pk => (
              <button key={pk} type="button" onClick={() => setProv(pk)} style={{
                padding: "4px 12px", borderRadius: 6, border: "1px solid var(--border-default)", cursor: "pointer",
                fontFamily: "inherit", fontSize: 12, fontWeight: 500,
                background: prov === pk ? "var(--accent)" : "transparent",
                color: prov === pk ? "#fff" : "var(--fg-secondary)",
              }}>{providers[pk].label}{providers[pk].hasKey ? "" : " ⚠️"}</button>
            ))}
            <div style={{ flex: 1 }}/>
            <button type="button" onClick={() => load(true)} disabled={liveBusy} title="Звірити зі списком моделей акаунта" style={{
              padding: "4px 10px", borderRadius: 6, border: "1px solid var(--border-default)", cursor: "pointer",
              fontFamily: "inherit", fontSize: 11.5, background: "transparent", color: "var(--fg-muted)",
            }}>{liveBusy ? "Перевіряю…" : (curProv?.liveChecked ? "✓ звірено" : "Перевірити доступність")}</button>
          </div>

          {/* Фільтри */}
          <div style={{ display: "flex", gap: 12, marginBottom: 8, flexWrap: "wrap", fontSize: 11.5, color: "var(--fg-secondary)" }}>
            {!imagesOnly && <label style={{ cursor: "pointer" }}><input type="checkbox" checked={fPhoto} onChange={e => setFPhoto(e.target.checked)}/> лише з фото</label>}
            {imagesOnly && <span style={{ color: "var(--fg-muted)" }}>лише моделі з підтримкою фото</span>}
            <label style={{ cursor: "pointer" }}><input type="checkbox" checked={fCheap} onChange={e => setFCheap(e.target.checked)}/> лише дешеві</label>
            <label style={{ cursor: "pointer" }}><input type="checkbox" checked={fLegacy} onChange={e => setFLegacy(e.target.checked)}/> показати legacy/deprecated</label>
          </div>
          <div style={{ fontSize: 10.5, color: "var(--fg-muted)", marginBottom: 6 }}>
            «/товар» — груба оцінка на 1 картку (~20k вхід / ~6.5k вихід сумарно: опис обраною моделлю + факти/vision на {fastModel?.label || "Haiku"}). Точна вартість — у результаті після генерації.
          </div>

          {/* За замовчуванням */}
          <button type="button" onMouseDown={e => { e.preventDefault(); onChange(null); setOpen(false); }} style={{
            width: "100%", textAlign: "left", padding: "7px 10px", border: 0, borderRadius: 6, cursor: "pointer",
            fontFamily: "inherit", fontSize: 12.5, marginBottom: 4,
            background: !value?.model ? "var(--bg-hover)" : "transparent", color: "var(--fg-primary)",
          }}>За замовчуванням (.env){data?.envDefault ? ` · зараз: ${data.envDefault.descModel}` : ""}</button>

          <div style={{ maxHeight: 320, overflowY: "auto" }}>
            {groups.length === 0 && <div style={{ padding: 10, fontSize: 12, color: "var(--fg-muted)" }}>Немає моделей під фільтри.</div>}
            {groups.map(({ group, items }) => (
              <div key={group} style={{ marginTop: 6 }}>
                <div style={{ fontSize: 10.5, fontWeight: 700, textTransform: "uppercase", letterSpacing: ".04em", color: "var(--fg-muted)", padding: "4px 10px" }}>{group}</div>
                {items.map(m => {
                  const dep = m.status === "deprecated" || m.status === "legacy";
                  const unavailable = m.available === false;
                  const selected = value?.model === m.id;
                  const est = tpProductCost(m, fastModel);
                  return (
                    <button key={m.id} type="button"
                      onMouseDown={e => { e.preventDefault(); pick(prov, m); }}
                      style={{
                        width: "100%", textAlign: "left", padding: "7px 10px", border: 0, borderRadius: 6, cursor: "pointer",
                        fontFamily: "inherit", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10,
                        background: selected ? "var(--accent)" : "transparent",
                        color: selected ? "#fff" : (dep ? "var(--fg-muted)" : "var(--fg-primary)"),
                        opacity: unavailable ? 0.55 : 1,
                      }}
                      onMouseEnter={e => { if (!selected) e.currentTarget.style.background = "var(--bg-hover)"; }}
                      onMouseLeave={e => { if (!selected) e.currentTarget.style.background = "transparent"; }}>
                      <span style={{ display: "flex", alignItems: "center", gap: 6, minWidth: 0 }}>
                        <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{m.label}</span>
                        {m.recommended && <span title="Рекомендована" style={{ flexShrink: 0 }}>⭐</span>}
                        {m.supportsImages && <span title="Підтримує фото" style={{ flexShrink: 0, fontSize: 11 }}>📷</span>}
                        {dep && <span style={{ flexShrink: 0, fontSize: 10.5, color: selected ? "#fff" : "var(--warning)" }}>може бути недоступна</span>}
                        {unavailable && <span style={{ flexShrink: 0, fontSize: 10.5, color: "var(--danger)" }}>немає на акаунті</span>}
                      </span>
                      <span style={{ flexShrink: 0, textAlign: "right", color: selected ? "rgba(255,255,255,.9)" : "var(--fg-secondary)" }}>
                        <span style={{ display: "block", fontSize: 12, fontWeight: 600 }}
                          title={est ? `опис ${tpFmtUsd(est.desc)} + факти/vision ≈ ${tpFmtUsd(est.total - est.desc)}` : ""}>
                          ≈{tpFmtUsd(est?.total)}/товар
                        </span>
                        <span style={{ display: "block", fontSize: 10, color: selected ? "rgba(255,255,255,.7)" : "var(--fg-muted)" }}>
                          in {tpPrice(m.inputPricePer1M)} · out {tpPrice(m.outputPricePer1M)} /1M
                        </span>
                      </span>
                    </button>
                  );
                })}
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

function TestPanel({ isMobile }) {
  const [mode, setMode]       = useStateTP("form");   // form | json
  const [form, setForm]       = useStateTP(tpEmptyForm);
  const [jsonText, setJson]   = useStateTP("");
  const [jsonErr, setJsonErr] = useStateTP(null);
  const [sending, setSending] = useStateTP(false);
  const [result, setResult]   = useStateTP(null);
  const [loadArt, setLoadArt] = useStateTP("");
  const [loading, setLoading] = useStateTP(false);
  const [rawLoaded, setRaw]   = useStateTP(null);
  const [tplMeta, setTplMeta] = useStateTP({});      // key -> { type, values[] }
  const [tplInfo, setTplInfo] = useStateTP(null);    // { path, productCount, count } | { error }
  const [tplLoading, setTplLoading] = useStateTP(false);
  const [aiUrl, setAiUrl]     = useStateTP("");   // посилання, по одному на рядок
  const [aiName, setAiName]   = useStateTP("");   // назва товару (необов'язково)
  const [aiLoading, setAiLoad] = useStateTP(false);
  const [aiInfo, setAiInfo]   = useStateTP(null);
  const [aiSearching, setAiSearching] = useStateTP(false);
  const [aiFound, setAiFound] = useStateTP(null);
  const [aiModel, setAiModel] = useStateTP(null);          // модель опису: { provider, model } | null (=.env)
  const [aiVisionModel, setAiVisionModel] = useStateTP(null); // модель фото/vision: { provider, model } | null (=.env)
  const [tpSuppliers, setTpSuppliers] = useStateTP([]);    // [{key,label}] для вибору постачальника-джерела
  useEffectTP(() => {
    fetch('/api/suppliers').then(r => r.json()).then(j => {
      if (j && j.ok && j.data) setTpSuppliers(Object.entries(j.data)
        .filter(([, c]) => c && c.active !== false)
        .map(([name, c]) => ({ key: c.cbPrefix || name, label: name })));
    }).catch(() => {});
  }, []);

  // Авто-пошук джерел через Serper (opts — явні значення для префілу з аудиту, інакше з форми)
  const findSources = async (opts = {}) => {
    const nm  = (opts.name != null ? opts.name : aiName) || "";
    const art = (opts.article != null ? opts.article : form.article) || "";
    if (!nm.trim() && !art.trim()) { setAiInfo({ error: "Вкажи назву або артикул для пошуку" }); return; }
    setAiSearching(true); setAiFound(null);
    try {
      const r = await API.findSources({ name: nm, article: art });
      setAiFound(r.results || []);
      const top = (r.results || []).slice(0, 10).map(x => x.url);
      if (top.length) setAiUrl(top.join("\n"));
    } catch (e) { setAiInfo({ error: "Пошук: " + e.message }); }
    finally { setAiSearching(false); }
  };
  const addSourceUrl = (url) => setAiUrl(prev => {
    const lines = prev.split(/\n/).map(s => s.trim()).filter(Boolean);
    if (lines.includes(url)) return prev;
    return [...lines, url].slice(0, 10).join("\n");
  });

  // Префіл AI-генератора ззовні (Прайси → аудит «Додати картку через AI»).
  // Ref завжди тримає найсвіжіше замикання (setForm/findSources), щоб глобалка не застрягла на стартовому рендері.
  const aiSectionRef = React.useRef(null);
  const prefillAIRef = React.useRef(null);
  prefillAIRef.current = ({ article, name, supplierPrefix, autoSearch } = {}) => {
    setMode("form");
    setForm(f => ({ ...f, article: article || "", supplierPrefix: supplierPrefix || f.supplierPrefix || "" }));
    setAiName(name || "");
    setAiUrl(""); setAiFound(null); setAiInfo(null); setResult(null);
    setTimeout(() => { try { aiSectionRef.current && aiSectionRef.current.scrollIntoView({ behavior: "smooth", block: "start" }); } catch {} }, 120);
    if (autoSearch && (name || article)) setTimeout(() => findSources({ name: name || "", article: article || "" }), 200);
  };
  useEffectTP(() => {
    window._tpPrefillAI = (p) => { prefillAIRef.current && prefillAIRef.current(p); };
    // Якщо вкладку Товари ще не відкривали — застосуємо відкладений префіл після монтування
    if (window._tpPendingAI) { const p = window._tpPendingAI; window._tpPendingAI = null; setTimeout(() => prefillAIRef.current && prefillAIRef.current(p), 60); }
    return () => { delete window._tpPrefillAI; };
  }, []);

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

  // AI-генератор картки: ссылка → текст сторінки → OpenAI → заповнення форми
  const aiGenerate = async () => {
    const urls = aiUrl.split(/[\n\s]+/).map(s => s.trim()).filter(s => /^https?:\/\//i.test(s)).slice(0, 10);
    if (!urls.length) { setAiInfo({ error: "Встав хоча б одне посилання (http...)" }); return; }
    setAiLoad(true);
    setAiInfo(null);
    try {
      const r = await API.aiGenerate({ urls, name: aiName, article: form.article, categoryId: form.parent_id, provider: aiModel?.provider, model: aiModel?.model, visionProvider: aiVisionModel?.provider, visionModel: aiVisionModel?.model });
      const p = r.product || {};
      const meta = {};
      (r.attributes || []).forEach(a => { meta[a.key] = { type: a.type, values: a.values || [] }; });
      if (r.attributes?.length) setTplMeta(meta);

      setForm(f => {
        const next = { ...f };
        const bi = v => ({ ua: (typeof v === "string" ? v : v?.ua) || "", ru: (typeof v === "string" ? "" : v?.ru) || "" });
        const uaOf = v => (typeof v === "string" ? v : (v && v.ua)) || "";
        if (p.title)             next.title = bi(p.title);
        if (p.mod_title)         next.mod_title = bi(p.mod_title);
        if (p.h1_title)          next.h1_title = bi(p.h1_title);
        if (p.short_description) next.short_description = bi(p.short_description);
        if (p.description)       next.description = bi(p.description);
        if (p.brand)             next.brand = p.brand;
        // gtin/mpn НЕ автозаповнюємо (лишаються вручну).
        // SEO-поля у формі — рядки (UA). RU генерується на бекенді, але форма поки тримає UA.
        if (p.seo_title)         next.seo_title = uaOf(p.seo_title);
        if (p.seo_keywords)      next.seo_keywords = uaOf(p.seo_keywords);
        if (p.seo_description)   next.seo_description = uaOf(p.seo_description);
        const chSrc = p.characteristics || {};
        if (r.attributes?.length) {
          next.chars = r.attributes.map(a => {
            const v = chSrc[a.key] || {};
            return { key: a.key, ua: (typeof v === "string" ? v : v?.ua) || "", ru: (typeof v === "string" ? "" : v?.ru) || "" };
          });
        } else {
          next.chars = Object.entries(chSrc).map(([k, v]) => ({ key: k, ua: (typeof v === "string" ? v : v?.ua) || "", ru: (typeof v === "string" ? "" : v?.ru) || "" }));
        }
        if (Array.isArray(p.images) && p.images.length) { next.img_links = p.images.join("\n"); next.img_override = false; }
        return next;
      });
      setAiInfo({ usage: r.usage, model: r.model, descModel: r.descModel, descProvider: r.descProvider, descError: r.descError, visionModel: r.visionModel, visionError: r.visionError, imgs: (p.images || []).length, descImagesCount: r.descImagesCount, chars: Object.keys(p.characteristics || {}).length, facts: r.facts, sources: r.sources, qa: r.qa, photos: r.photos });
    } catch (e) {
      setAiInfo({ error: e.message });
    } finally {
      setAiLoad(false);
    }
  };

  // Підтягнути шаблон (ключі характеристик) саме цієї категорії та влити в редактор
  const loadCategoryTemplate = async (categoryId) => {
    if (!categoryId) { setTplInfo({ error: "Спершу обери категорію" }); return; }
    setTplLoading(true);
    try {
      const r = await API.getCategoryTemplate(categoryId);
      if (!r.found || !r.attributes?.length) {
        setTplInfo({ error: `Для категорії #${categoryId} немає даних у каталозі (немає товарів — нема з чого зібрати шаблон).` });
        return;
      }
      const meta = {};
      r.attributes.forEach(a => { meta[a.key] = { type: a.type, values: a.values || [] }; });
      setTplMeta(meta);
      let kept = 0;
      setForm(f => {
        const existing = new Map((f.chars || []).map(c => [c.key, c]));
        // chars = РІВНО ключі цієї категорії; значення зберігаємо лише для співпадаючих ключів
        const merged = r.attributes.map(a => {
          const ex = existing.get(a.key);
          if (ex && (ex.ua || ex.ru)) kept++;
          return { key: a.key, ua: ex?.ua || "", ru: ex?.ru || "" };
        });
        return { ...f, chars: merged };
      });
      setTplInfo({ path: r.category?.path, productCount: r.category?.productCount, count: r.attributes.length, kept });
    } catch (e) {
      setTplInfo({ error: e.message });
    } finally {
      setTplLoading(false);
    }
  };

  // Динамічні характеристики
  const addChar = () => setForm(f => ({ ...f, chars: [...(f.chars || []), { key: "", ua: "", ru: "" }] }));
  const updChar = (i, field, val) => setForm(f => {
    const chars = [...(f.chars || [])];
    chars[i] = { ...chars[i], [field]: val };
    return { ...f, chars };
  });
  const delChar = (i) => setForm(f => ({ ...f, chars: (f.chars || []).filter((_, j) => j !== i) }));

  // Обʼєкт продукту з форми (для прев'ю та відправки)
  const builtProduct = useMemoTP(() => tpBuildProduct(form), [form]);

  // Перемкнути режим: при вході в JSON — підставити поточний продукт
  const switchMode = (m) => {
    if (m === "json") {
      setJson(JSON.stringify([builtProduct], null, 2));
      setJsonErr(null);
    }
    setMode(m);
  };

  // Завантажити поточний товар з сайту за артикулом → префіл форми
  const loadFromSite = async () => {
    const art = loadArt.trim();
    if (!art) return;
    setLoading(true);
    setRaw(null);
    try {
      const r = await API.findHoroshopProduct(art);
      if (r.found && (r.raw || r.data)) {
        const raw = r.raw || r.data;
        const upd = tpPrefill(raw, art);
        setForm(f => ({ ...f, ...upd }));
        setRaw(raw);
        const titleStr = typeof raw.title === "string" ? raw.title : (tpLang(raw.title)?.ua || art);
        setResult({ info: `Завантажено: ${titleStr} (id ${raw.id ?? "?"}). Поля форми оновлено — звір нижче «сирі дані з сайту».` });
      } else {
        setResult({ info: `Товар з артикулом "${art}" не знайдено в каталозі (можна створити новий).` });
      }
    } catch (e) {
      setResult({ error: e.message });
    } finally {
      setLoading(false);
    }
  };

  // Зібрати products[] для відправки
  const collectProducts = () => {
    if (mode === "json") {
      let parsed;
      try { parsed = JSON.parse(jsonText); }
      catch (e) { setJsonErr("Невалідний JSON: " + e.message); return null; }
      setJsonErr(null);
      const arr = Array.isArray(parsed) ? parsed : [parsed];
      if (!arr.length) { setJsonErr("Порожній масив товарів"); return null; }
      return arr;
    }
    if (!builtProduct.article) { setResult({ error: "Артикул обовʼязковий" }); return null; }
    return [builtProduct];
  };

  const send = async () => {
    const products = collectProducts();
    if (!products) return;
    setSending(true);
    setResult(null);
    try {
      const r = await API.horoshopImport(products);
      setResult(r);
    } catch (e) {
      setResult({ error: e.message });
    } finally {
      setSending(false);
    }
  };

  const resetForm = () => { setForm(tpEmptyForm()); setResult(null); };

  if (isMobile) {
    return (
      <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", padding: 32, textAlign: "center", color: "var(--fg-muted)" }}>
        <div>
          <Icon name="monitor" size={40} style={{ opacity: 0.3 }}/>
          <p style={{ marginTop: 12, fontSize: 14 }}>Тест-панель доступна лише на компʼютері.</p>
        </div>
      </div>
    );
  }

  return (
    <div style={{ flex: 1, overflowY: "auto", padding: "20px 24px 60px" }}>
      <div style={{ maxWidth: 980, margin: "0 auto" }}>

        {/* Хедер + перемикач режиму */}
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 8 }}>
          <div>
            <h2 style={{ fontSize: 18, fontWeight: 600, color: "var(--fg-primary)", margin: 0 }}>Тест-панель імпорту</h2>
            <p style={{ fontSize: 12.5, color: "var(--fg-muted)", margin: "4px 0 0" }}>
              Додавання / оновлення товарів через Horoshop <code>catalog/import</code>. Якщо артикул існує — товар оновлюється, інакше створюється новий.
            </p>
          </div>
          <div style={{ display: "flex", background: "var(--bg-raised)", borderRadius: 8, border: "1px solid var(--border-default)", padding: 3, gap: 2, flexShrink: 0 }}>
            {[["form", "Форма"], ["json", "JSON"]].map(([k, lbl]) => (
              <button key={k} onClick={() => switchMode(k)} style={{
                padding: "6px 16px", border: 0, borderRadius: 6, cursor: "pointer", fontFamily: "inherit",
                fontSize: 12, fontWeight: 500,
                background: mode === k ? "var(--accent)" : "transparent",
                color: mode === k ? "#fff" : "var(--fg-secondary)",
              }}>{lbl}</button>
            ))}
          </div>
        </div>

        {/* Кеш каталогу (тест-режим) */}
        <TPCatalogStatus/>

        {/* Префіл з сайту */}
        <div style={{ display: "flex", gap: 8, alignItems: "flex-end", margin: "16px 0", padding: 12, background: "var(--bg-panel)", border: "1px solid var(--border-subtle)", borderRadius: 12 }}>
          <div style={{ flex: 1, maxWidth: 320 }}>
            <TPLabel hint="завантажить назву/ціну/наявність">Префіл з сайту за артикулом</TPLabel>
            <TPInput value={loadArt} onChange={setLoadArt} placeholder="Артикул товару на сайті"/>
          </div>
          <Button variant="secondary" leftIcon="download" onClick={loadFromSite} disabled={loading || !loadArt.trim()}>
            {loading ? "Шукаю…" : "Завантажити"}
          </Button>
        </div>

        {/* 🤖 AI-генератор картки */}
        <div ref={aiSectionRef} style={{ margin: "16px 0", padding: 14, background: "var(--bg-panel)", border: "1px solid var(--border-subtle)", borderRadius: 12 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
            <Icon name="sparkles" size={16} color="var(--accent)"/>
            <span style={{ fontSize: 13, fontWeight: 600, color: "var(--fg-primary)" }}>AI-генератор картки</span>
          </div>
          <p style={{ fontSize: 11.5, color: "var(--fg-muted)", margin: "0 0 10px" }}>
            Встав 1-3 посилання (виробник / Rozetka / конкурент) — по одному на рядок. Збере й зіллє дані з усіх. Використає <b>артикул</b> і <b>категорію</b> з форми. Заповнить назву, опис, характеристики за шаблоном і фото — далі перевіриш і надішлеш.
          </p>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 5 }}>
            <TPLabel hint="по одному на рядок, до 3">Посилання-джерела</TPLabel>
            <Button variant="secondary" size="sm" leftIcon="search" onClick={() => findSources()} disabled={aiSearching || (!aiName.trim() && !form.article.trim())}>
              {aiSearching ? "Шукаю…" : "Знайти джерела"}
            </Button>
          </div>
          <TPTextarea value={aiUrl} onChange={setAiUrl} rows={3} mono
            placeholder={"https://touch.com.ua/...\nhttps://rozetka.com.ua/...\nhttps://..."}/>
          {Array.isArray(aiFound) && (
            <div style={{ marginTop: 6, display: "flex", flexDirection: "column", gap: 3 }}>
              {aiFound.length === 0 && <span style={{ fontSize: 11.5, color: "var(--fg-muted)" }}>Нічого не знайдено — уточни назву.</span>}
              {aiFound.map((r, i) => {
                let host = r.url; try { host = new URL(r.url).host.replace(/^www\./, ""); } catch {}
                return (
                  <button key={i} type="button" onClick={() => addSourceUrl(r.url)} title={r.url} style={{
                    textAlign: "left", border: 0, background: "transparent", cursor: "pointer", padding: "2px 0",
                    fontSize: 11.5, color: "var(--fg-secondary)", display: "flex", gap: 6, alignItems: "baseline", fontFamily: "inherit",
                  }}>
                    <span style={{ color: "var(--accent)", flexShrink: 0 }}>+ {host}</span>
                    <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{r.title}</span>
                  </button>
                );
              })}
            </div>
          )}
          <div style={{ height: 10 }}/>
          <TPModelPicker value={aiModel} onChange={setAiModel}/>
          <div style={{ height: 10 }}/>
          <TPModelPicker value={aiVisionModel} onChange={setAiVisionModel} imagesOnly
            label="AI-модель для фото (vision)"
            hint="якою моделлю відбирати/класифікувати фото — можна міксувати з моделлю опису"/>
          <div style={{ height: 10 }}/>
          <div style={{ display: "flex", gap: 8, alignItems: "flex-end" }}>
            <div style={{ flex: 1 }}>
              <TPLabel hint="необов'язково — інакше візьме зі сторінки">Назва товару (вручну)</TPLabel>
              <TPInput value={aiName} onChange={setAiName} placeholder="напр. Ноутбук ASUS TUF Gaming F15"/>
            </div>
            <Button variant="primary" leftIcon="sparkles" onClick={aiGenerate} disabled={aiLoading || !aiUrl.trim()}>
              {aiLoading ? "Генерую…" : "Згенерувати"}
            </Button>
          </div>
          {!form.parent_id && (
            <div style={{ fontSize: 11.5, color: "var(--warning)", marginTop: 8 }}>
              ⚠️ Категорію не обрано — характеристики не заповняться. Обери категорію у секції «Категорії».
            </div>
          )}
          {aiInfo?.error && <div style={{ fontSize: 12.5, color: "var(--danger)", marginTop: 10 }}>{aiInfo.error}</div>}
          {aiInfo?.descError && (
            <div style={{ fontSize: 12.5, color: "var(--warning)", marginTop: 10, padding: "8px 10px", borderRadius: 8, border: "1px solid rgba(245,158,11,.4)" }}>
              ⚠️ {aiInfo.descError}
            </div>
          )}
          {aiInfo?.visionError && (
            <div style={{ fontSize: 12.5, color: "var(--warning)", marginTop: 10, padding: "8px 10px", borderRadius: 8, border: "1px solid rgba(245,158,11,.4)" }}>
              ⚠️ {aiInfo.visionError}
            </div>
          )}
          {aiInfo && !aiInfo.error && (
            <div style={{ fontSize: 12, color: "var(--credit)", marginTop: 10 }}>
              ✓ Готово · модель {aiInfo.model} · характеристик: {aiInfo.chars} · фото: {aiInfo.imgs}{typeof aiInfo.descImagesCount === "number" ? ` · в опис: ${aiInfo.descImagesCount}` : ""}
              {aiInfo.usage && (() => {
                const u = aiInfo.usage;
                const cost = (typeof u.cost_usd === "number") ? u.cost_usd : tpAiCost(aiInfo.model, u);
                const total15k = (cost != null) ? cost * 15000 : null;
                return (
                  <span style={{ color: "var(--fg-muted)" }}>
                    {" "}· токени {u.prompt_tokens}+{u.completion_tokens}
                    {typeof u.vision_tokens === "number" ? ` (vision ${u.vision_tokens})` : ""}
                    {cost != null ? ` · ≈$${cost.toFixed(4)}/шт` : ""}
                    {total15k != null ? ` · ~$${Math.round(total15k)} за 15k` : ""}
                  </span>
                );
              })()}
              {aiInfo.facts && (
                <span style={{ color: "var(--fg-muted)", display: "block", marginTop: 2 }}>
                  Назва: {aiInfo.facts.name || "—"} · знайдено спеків зі сторінок: {aiInfo.facts.specsCount}
                </span>
              )}
              {Array.isArray(aiInfo.sources) && aiInfo.sources.map((s, i) => (
                <span key={i} style={{ color: s.error ? "var(--warning)" : "var(--fg-muted)", display: "block", marginTop: 1 }}>
                  {s.error ? `⚠️ ${s.url}: ${s.error}` : `• джерело ${i + 1}: фото ${s.images}${typeof s.content === "number" ? ` (+${s.content} з опису)` : ""}, спеків ${s.specs}`}
                </span>
              ))}
              {Array.isArray(aiInfo.photos) && aiInfo.photos.length > 0 && (() => {
                const roles = {};
                aiInfo.photos.forEach(p => { const k = p.relevant === false ? "відсіяно" : (p.role || "?"); roles[k] = (roles[k] || 0) + 1; });
                return <span style={{ color: "var(--fg-muted)", display: "block", marginTop: 2 }}>
                  Фото (vision): {Object.entries(roles).map(([k, v]) => `${k}:${v}`).join(" · ")}
                </span>;
              })()}
              <span style={{ color: "var(--fg-muted)", display: "block", marginTop: 2 }}>Перевір поля нижче перед надсиланням.</span>
            </div>
          )}
          {aiInfo && aiInfo.qa && (
            <div style={{ marginTop: 10, padding: "10px 12px", borderRadius: 8,
              border: "1px solid " + (aiInfo.qa.verdict === "ready" ? "rgba(16,185,129,.4)" : aiInfo.qa.verdict === "review" ? "rgba(245,158,11,.4)" : "rgba(244,63,94,.4)"),
              background: "color-mix(in oklab," + (aiInfo.qa.verdict === "ready" ? "var(--credit)" : aiInfo.qa.verdict === "review" ? "var(--warning)" : "var(--danger)") + " 8%, transparent)" }}>
              <div style={{ fontSize: 12.5, fontWeight: 600, color: "var(--fg-primary)" }}>
                Якість картки: {aiInfo.qa.score}/100 · {aiInfo.qa.verdict === "ready" ? "✅ готово" : aiInfo.qa.verdict === "review" ? "🟡 на перевірку" : "🔴 слабко"}
              </div>
              {Array.isArray(aiInfo.qa.issues) && aiInfo.qa.issues.length > 0 && (
                <ul style={{ margin: "6px 0 0", paddingLeft: 18, fontSize: 11.5, color: "var(--fg-muted)" }}>
                  {aiInfo.qa.issues.slice(0, 8).map((it, i) => <li key={i}>{it}</li>)}
                </ul>
              )}
            </div>
          )}
        </div>

        {/* Сирі дані з сайту — щоб бачити, що реально повертає catalog/export */}
        {rawLoaded && (
          <TPSection title={`Сирі дані з сайту (${Object.keys(rawLoaded).length} полів)`} defaultOpen={false}>
            <pre style={{ margin: 0, padding: 12, background: "var(--bg-base)", borderRadius: 8, fontSize: 12,
              fontFamily: "var(--font-mono, monospace)", color: "var(--fg-secondary)", overflowX: "auto", maxHeight: 360 }}>
              {JSON.stringify(rawLoaded, null, 2)}
            </pre>
          </TPSection>
        )}

        {/* ── Режим ФОРМА ── */}
        {mode === "form" && (
          <>
            <TPSection title="Основне">
              <TPGrid>
                <div>
                  <TPLabel hint="обовʼязковий">Артикул (article)</TPLabel>
                  <TPInput value={form.article} onChange={v => upd("article", v)} placeholder="напр. AAAA_TEST"/>
                </div>
                <div>
                  <TPLabel hint="для модифікацій">Батьківський артикул (parent_article)</TPLabel>
                  <TPInput value={form.parent_article} onChange={v => upd("parent_article", v)} placeholder="порожньо = сам товар"/>
                </div>
              </TPGrid>
              <div style={{ height: 12 }}/>
              <TPBiField label="Назва (title)" hint="обовʼязкова для нового товару" val={form.title} onChange={v => upd("title", v)}/>
              <div style={{ height: 12 }}/>
              <TPGrid>
                <div>
                  <TPLabel>Бренд (brand)</TPLabel>
                  <TPInput value={form.brand} onChange={v => upd("brand", v)}/>
                </div>
                <div>
                  <TPLabel hint="опціонально">Артикул для показу</TPLabel>
                  <TPInput value={form.article_for_display} onChange={v => upd("article_for_display", v)}/>
                </div>
              </TPGrid>
              <div style={{ height: 12 }}/>
              <TPBiField label="Назва модифікації (mod_title)" val={form.mod_title} onChange={v => upd("mod_title", v)}/>
            </TPSection>

            <TPSection title="Ціна та наявність">
              <TPGrid cols={3}>
                <div>
                  <TPLabel>Ціна (price)</TPLabel>
                  <TPInput type="number" value={form.price} onChange={v => upd("price", v)}/>
                </div>
                <div>
                  <TPLabel>Стара ціна (price_old)</TPLabel>
                  <TPInput type="number" value={form.price_old} onChange={v => upd("price_old", v)}/>
                </div>
                <div>
                  <TPLabel hint="%">Знижка (discount)</TPLabel>
                  <TPInput type="number" value={form.discount} onChange={v => upd("discount", v)}/>
                </div>
              </TPGrid>
              <div style={{ height: 12 }}/>
              <TPGrid>
                <div>
                  <TPLabel>Валюта (currency)</TPLabel>
                  <select value={form.currency} onChange={e => upd("currency", e.target.value)} style={tpInputStyle}>
                    {["UAH", "USD", "EUR"].map(c => <option key={c} value={c}>{c}</option>)}
                  </select>
                </div>
                <div>
                  <TPLabel hint="лише при вимкненому обліку залишків">Наявність (presence)</TPLabel>
                  <select value={form.presence} onChange={e => upd("presence", e.target.value)} style={tpInputStyle}>
                    <option value="">(не вказувати)</option>
                    {TP_PRESENCE.map(v => <option key={v} value={v}>{v}</option>)}
                  </select>
                </div>
              </TPGrid>
              <div style={{ height: 14 }}/>
              <div>
                <TPLabel hint="запишеться постачальником товару на сайті (Horoshop supplier по id)">Постачальник-джерело</TPLabel>
                <select value={form.supplierPrefix} onChange={e => upd("supplierPrefix", e.target.value)} style={tpInputStyle}>
                  <option value="">— (не вказувати)</option>
                  {tpSuppliers.map(s => <option key={s.key} value={s.key}>{s.label}</option>)}
                </select>
              </div>
              <div style={{ height: 14 }}/>
              <TPToggle value={form.display_in_showcase} onChange={v => upd("display_in_showcase", v)} label="Відображати товар на сайті (display_in_showcase)"/>
            </TPSection>

            <TPSection title="Категорії" defaultOpen={false}>
              <TPCategoryPicker valueId={form.parent_id} valuePath={form.parent}
                onPick={(id, path) => { setForm(f => ({ ...f, parent_id: String(id), parent: path })); loadCategoryTemplate(id); }}/>
              <div style={{ height: 12 }}/>
              <TPGrid>
                <div>
                  <TPLabel hint="має пріоритет">ID розділу (parent.id)</TPLabel>
                  <TPInput type="number" value={form.parent_id} onChange={v => upd("parent_id", v)} placeholder="напр. 1036"/>
                </div>
                <div>
                  <TPLabel hint="якщо немає ID">Шлях розділу (parent)</TPLabel>
                  <TPInput value={form.parent} onChange={v => upd("parent", v)} placeholder="iPhone / iPhone 6"/>
                </div>
              </TPGrid>
              <div style={{ height: 12 }}/>
              <TPLabel hint="по одному на рядок: шлях або ID">Додаткові розділи (alt_parent)</TPLabel>
              <TPTextarea value={form.alt_parent} onChange={v => upd("alt_parent", v)} rows={3}
                placeholder={"Другие товары / Аксессуары\n98\nApple / Test"}/>
            </TPSection>

            <TPSection title="Опис" defaultOpen={false}>
              <TPBiField label="Короткий опис (short_description)" val={form.short_description} onChange={v => upd("short_description", v)} textarea rows={2}/>
              <div style={{ height: 12 }}/>
              <TPBiField label="Повний опис (description)" hint="можна HTML" val={form.description} onChange={v => upd("description", v)} textarea rows={4}/>
            </TPSection>

            <TPSection title="SEO" defaultOpen={false}>
              <TPLabel>SEO заголовок (seo_title)</TPLabel>
              <TPInput value={form.seo_title} onChange={v => upd("seo_title", v)}/>
              <div style={{ height: 12 }}/>
              <TPLabel>SEO ключові слова (seo_keywords)</TPLabel>
              <TPInput value={form.seo_keywords} onChange={v => upd("seo_keywords", v)}/>
              <div style={{ height: 12 }}/>
              <TPLabel>SEO опис (seo_description)</TPLabel>
              <TPTextarea value={form.seo_description} onChange={v => upd("seo_description", v)} rows={2}/>
              <div style={{ height: 12 }}/>
              <TPBiField label="H1 заголовок (h1_title)" hint="назва без артикула" val={form.h1_title} onChange={v => upd("h1_title", v)}/>
            </TPSection>

            <TPSection title="Атрибути" defaultOpen={false}>
              <TPGrid cols={3}>
                <div><TPLabel>Колір (color)</TPLabel><TPInput value={form.color} onChange={v => upd("color", v)}/></div>
                <div><TPLabel>Штрихкод (gtin)</TPLabel><TPInput value={form.gtin} onChange={v => upd("gtin", v)}/></div>
                <div><TPLabel>Код виробника (mpn)</TPLabel><TPInput value={form.mpn} onChange={v => upd("mpn", v)}/></div>
              </TPGrid>
              <div style={{ height: 12 }}/>
              <TPGrid cols={3}>
                <div><TPLabel>Популярність</TPLabel><TPInput type="number" value={form.popularity} onChange={v => upd("popularity", v)}/></div>
                <div><TPLabel>Тип гарантії</TPLabel>
                  <select value={form.guarantee_shop} onChange={e => upd("guarantee_shop", e.target.value)} style={tpInputStyle}>
                    <option value="">(не вказувати)</option>
                    {TP_GUARANTEE_SHOP.map(v => <option key={v} value={v}>{v}</option>)}
                  </select>
                </div>
                <div><TPLabel hint="міс. — задається під постачальника">Гарантія</TPLabel><TPInput type="number" value={form.guarantee_length} onChange={v => upd("guarantee_length", v)}/></div>
              </TPGrid>
              <div style={{ height: 12 }}/>
              <TPGrid cols={3}>
                <div><TPLabel>УКТ ЗЕД (uktzed)</TPLabel><TPInput value={form.uktzed} onChange={v => upd("uktzed", v)}/></div>
                <div><TPLabel>Стан (condition)</TPLabel>
                  <select value={form.condition} onChange={e => upd("condition", e.target.value)} style={tpInputStyle}>
                    <option value="">(не вказувати)</option>
                    {TP_CONDITION.map(v => <option key={v} value={v}>{v}</option>)}
                  </select>
                </div>
                <div><TPLabel hint="через кому">Іконки (icons)</TPLabel><TPInput value={form.icons} onChange={v => upd("icons", v)} list="tp-icons" placeholder="Хіт, Новинка"/>
                  <datalist id="tp-icons">{TP_ICONS.map(v => <option key={v} value={v}/>)}</datalist>
                </div>
              </TPGrid>
              <div style={{ height: 14 }}/>
              <TPLabel hint="вивантаження на маркетплейси (export_to_marketplace)">Маркетплейси</TPLabel>
              <div style={{ display: "flex", gap: 16, flexWrap: "wrap", marginTop: 4 }}>
                {TP_MARKETPLACES.map(m => {
                  const on = (form.export_to_marketplace || []).includes(m);
                  return (
                    <label key={m} style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 12.5, color: "var(--fg-secondary)", cursor: "pointer" }}>
                      <input type="checkbox" checked={on} onChange={e => {
                        const cur = form.export_to_marketplace || [];
                        upd("export_to_marketplace", e.target.checked ? [...cur, m] : cur.filter(x => x !== m));
                      }}/>{m}
                    </label>
                  );
                })}
              </div>
            </TPSection>

            <TPSection title={`Характеристики${(form.chars || []).length ? ` (${form.chars.length})` : ""}`} defaultOpen={false}>
              {/* Дії: підтягнути шаблон обраної категорії */}
              <div style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 10, flexWrap: "wrap" }}>
                <Button variant="secondary" size="sm" leftIcon="layout-template"
                  onClick={() => loadCategoryTemplate(form.parent_id)} disabled={tplLoading || !form.parent_id}>
                  {tplLoading ? "Підтягую…" : "Підтягнути шаблон категорії"}
                </Button>
                {tplInfo && !tplInfo.error && (
                  <span style={{ fontSize: 12, color: "var(--credit)" }}>
                    ✓ {tplInfo.path} · {tplInfo.count} ключів (з {tplInfo.productCount} товарів){tplInfo.kept ? ` · збережено значень: ${tplInfo.kept}` : ""}
                  </span>
                )}
                {tplInfo?.error && <span style={{ fontSize: 12, color: "var(--warning)" }}>{tplInfo.error}</span>}
              </div>

              {(form.chars || []).length > 0 && (
                <div style={{ display: "grid", gridTemplateColumns: "1.3fr 1fr 1fr 32px", gap: 8, marginBottom: 6, fontSize: 11, fontWeight: 500, color: "var(--fg-muted)" }}>
                  <span>Ключ (як на сайті)</span><span>🇺🇦 укр</span><span>🇷🇺 рус</span><span/>
                </div>
              )}
              {(form.chars || []).map((c, i) => {
                const meta = tplMeta[c.key];
                const dlId = meta && meta.values?.length ? `tplv-${i}` : undefined;
                return (
                  <div key={i} style={{ marginBottom: 8 }}>
                    <div style={{ display: "grid", gridTemplateColumns: "1.3fr 1fr 1fr 32px", gap: 8, alignItems: "center" }}>
                      <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
                        <TPInput value={c.key} onChange={v => updChar(i, "key", v)} placeholder="напр. garantija"/>
                        {meta && (
                          <span title={meta.type === "choice" ? "вибір зі списку (значення зі справочника)" : "вільний текст"}
                            style={{ fontSize: 10, fontWeight: 600, padding: "2px 5px", borderRadius: 5, flexShrink: 0,
                              background: meta.type === "choice" ? "var(--accent-soft, rgba(99,102,241,.15))" : "var(--bg-raised)",
                              color: meta.type === "choice" ? "var(--accent)" : "var(--fg-muted)" }}>
                            {meta.type === "choice" ? "list" : "txt"}
                          </span>
                        )}
                      </div>
                      <TPInput value={c.ua} onChange={v => updChar(i, "ua", v)} placeholder="значення укр" list={dlId}/>
                      <TPInput value={c.ru} onChange={v => updChar(i, "ru", v)} placeholder="значення рус" list={dlId}/>
                      <button type="button" onClick={() => delChar(i)} title="Видалити" style={{
                        width: 32, height: 36, border: "1px solid var(--border-default)", borderRadius: 8, cursor: "pointer",
                        background: "transparent", color: "var(--fg-muted)", display: "flex", alignItems: "center", justifyContent: "center",
                      }}><Icon name="trash-2" size={15}/></button>
                    </div>
                    {dlId && (
                      <datalist id={dlId}>
                        {meta.values.map((v, vi) => <option key={vi} value={v}/>)}
                      </datalist>
                    )}
                  </div>
                );
              })}
              {(form.chars || []).length === 0 && (
                <p style={{ fontSize: 12.5, color: "var(--fg-muted)", margin: "0 0 10px" }}>
                  Порожньо. Обери категорію вище (підтягне ключі шаблона) або завантаж товар з сайту.
                </p>
              )}
              <Button variant="ghost" size="sm" leftIcon="plus" onClick={addChar}>Додати характеристику</Button>
            </TPSection>

            <TPSection title="Зображення" defaultOpen={false}>
              <TPLabel hint="по одному URL на рядок, ≤5МБ кожне">Посилання (images.links)</TPLabel>
              <TPTextarea value={form.img_links} onChange={v => upd("img_links", v)} rows={3} mono
                placeholder={"https://...\nhttps://..."}/>
              <div style={{ height: 14, display: "flex" }}/>
              <div style={{ display: "flex", gap: 24 }}>
                <TPToggle value={form.img_override} onChange={v => upd("img_override", v)} label="override (видалити старі перед завантаженням)"/>
                <TPToggle value={form.img_removeAll} onChange={v => upd("img_removeAll", v)} label="removeAll (очистити галерею)"/>
              </div>
            </TPSection>

            {/* Прев'ю JSON що буде відправлено */}
            <TPSection title="JSON, що буде відправлено" defaultOpen={false}>
              <pre style={{ margin: 0, padding: 12, background: "var(--bg-base)", borderRadius: 8, fontSize: 12,
                fontFamily: "var(--font-mono, monospace)", color: "var(--fg-secondary)", overflowX: "auto", maxHeight: 320 }}>
                {JSON.stringify(builtProduct, null, 2)}
              </pre>
            </TPSection>
          </>
        )}

        {/* ── Режим JSON ── */}
        {mode === "json" && (
          <div style={{ marginBottom: 14 }}>
            <TPLabel hint="масив товарів products[] або один обʼєкт">Сирий JSON</TPLabel>
            <TPTextarea value={jsonText} onChange={v => { setJson(v); setJsonErr(null); }} rows={22} mono/>
            {jsonErr && <div style={{ marginTop: 8, fontSize: 12.5, color: "var(--danger)" }}>{jsonErr}</div>}
          </div>
        )}

        {/* Кнопки дій */}
        <div style={{ display: "flex", gap: 10, marginTop: 4, alignItems: "center" }}>
          <Button variant="primary" leftIcon="upload" onClick={send} disabled={sending}>
            {sending ? "Надсилаю…" : "Надіслати в каталог"}
          </Button>
          {mode === "form" && (
            <Button variant="ghost" leftIcon="rotate-ccw" onClick={resetForm}>Очистити форму</Button>
          )}
          <span style={{ fontSize: 12, color: "var(--fg-muted)", marginLeft: "auto" }}>
            ⚠️ Зміни застосовуються до реального каталогу сайту
          </span>
        </div>

        {/* Результат */}
        {result && <TPResult result={result}/>}

      </div>
    </div>
  );
}

// ── Панель результату ──────────────────────────────────────────────
function TPResult({ result }) {
  if (result.info)  return <TPBanner color="var(--accent)" icon="info" text={result.info}/>;
  if (result.error) return <TPBanner color="var(--danger)" icon="alert-triangle" text={result.error}/>;

  const status = result.status || "?";
  const statusColor = status === "OK" ? "var(--credit)" : status === "WARNING" ? "var(--warning)" : "var(--danger)";
  const log = result.response?.log || [];

  return (
    <div style={{ marginTop: 18, border: "1px solid var(--border-subtle)", borderRadius: 12, background: "var(--bg-panel)", overflow: "hidden" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "12px 16px", borderBottom: "1px solid var(--border-subtle)" }}>
        <span style={{ fontSize: 13, fontWeight: 600, color: "var(--fg-primary)" }}>Відповідь</span>
        <span style={{ fontSize: 11.5, fontWeight: 600, color: statusColor, padding: "2px 8px", borderRadius: 6,
          background: "color-mix(in oklab," + statusColor + " 14%, transparent)" }}>{status}</span>
        <span style={{ fontSize: 12, color: "var(--fg-muted)", marginLeft: "auto" }}>надіслано: {result.sent ?? "—"}</span>
      </div>
      <div style={{ padding: "8px 16px 14px" }}>
        {log.length === 0 && <div style={{ fontSize: 13, color: "var(--fg-muted)", padding: "8px 0" }}>Порожній журнал.</div>}
        {log.map((entry, i) => (
          <div key={i} style={{ padding: "10px 0", borderBottom: i < log.length - 1 ? "1px solid var(--border-subtle)" : "none" }}>
            <div style={{ fontSize: 12.5, fontWeight: 600, color: "var(--fg-primary)", marginBottom: 6, fontFamily: "var(--font-mono, monospace)" }}>
              {entry.article}
            </div>
            <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
              {(entry.info || []).map((it, j) => (
                <div key={j} style={{ display: "flex", gap: 8, alignItems: "flex-start", fontSize: 12.5 }}>
                  <span style={{ flexShrink: 0, width: 26, textAlign: "right", fontWeight: 600, fontFamily: "var(--font-mono, monospace)", color: tpCodeColor(it.code) }}>
                    {it.code}
                  </span>
                  <span style={{ color: "var(--fg-secondary)" }}>{it.message}</span>
                </div>
              ))}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

function TPBanner({ color, icon, text }) {
  return (
    <div style={{ marginTop: 18, display: "flex", gap: 10, alignItems: "flex-start", padding: "12px 16px", borderRadius: 12,
      background: "color-mix(in oklab," + color + " 10%, transparent)", border: "1px solid color-mix(in oklab," + color + " 30%, transparent)" }}>
      <Icon name={icon} size={18} style={{ color, flexShrink: 0, marginTop: 1 }}/>
      <span style={{ fontSize: 13, color: "var(--fg-primary)", whiteSpace: "pre-wrap" }}>{text}</span>
    </div>
  );
}

window.TestPanel = TestPanel;
