// pages-system.jsx — Requisites + Content + Settings (NEW)

const { useState: useStateS, useMemo: useMemoS, useRef: useRefS, useEffect: useEffectS } = React;

/* ============================
   REQUISITES — NEW
============================ */

const TYPE_META = {
  card:    { icon: "card",    label: "Карта",       color: "from-indigo-300 to-indigo-500" },
  crypto:  { icon: "bitcoin", label: "Крипто",      color: "from-amber-400 to-amber-600" },
  stars:   { icon: "star",    label: "TG Stars",    color: "from-rose-400 to-rose-600" },
  payment: { icon: "card",    label: "PayPal/иное", color: "from-emerald-400 to-emerald-600" },
};

function RequisitesPage() {
  const [list, setList] = useStateS([]);
  const [loading, setLoading] = useStateS(true);
  const [selected, setSelected] = useStateS(null);
  const [filter, setFilter] = useStateS("all");
  // Группа реквизитов: "" = общие (мои аккаунты без владельца), иначе ownerId
  // владельца (напр. Рома). Каждый владелец может иметь свой набор карт; его
  // аккаунты в боте принимают оплату на них (workspace резолвит по ownerId).
  const [group, setGroup] = useStateS("");
  const [groups, setGroups] = useStateS([]);
  const [dirty, setDirty] = useStateS(false);

  const loadGroups = React.useCallback(async () => {
    setGroups(await fetchPaymentGroups());
  }, []);
  useEffectS(() => { loadGroups(); }, [loadGroups]);

  const activeGroup = groups.find((g) => g.id === group) || groups[0] || { id: "", name: "Общие", own: true, count: 0 };
  // Редактируем, если это общие ИЛИ владелец включил свои реквизиты. Иначе показ
  // копии общих как превью (без записи), пока не нажал «Задать свои».
  const editable = !group || !!activeGroup.own;

  // ВАЖНО: НЕТ polling — это страница редактирования. Polling перезатирал бы ввод.
  const reload = React.useCallback(async () => {
    setLoading(true);
    const data = await fetchPayments(group);
    setList(data);
    setLoading(false);
    setDirty(false);
    setSelected((cur) => cur || (data[0] ? data[0].id : null));
  }, [group]);

  useEffectS(() => {
    let stopped = false;
    setLoading(true);
    (async () => {
      const data = await fetchPayments(group);
      if (stopped) return;
      setList(data);
      setLoading(false);
      setDirty(false);
      setSelected((cur) => cur || (data[0] ? data[0].id : null));
    })();
    return () => { stopped = true; };
  }, [group]);

  const onToggleOwn = async (enabled) => {
    if (!group) return;
    await toggleOwnerPayments(group, enabled);
    await loadGroups();
    await reload();
  };

  const filtered = useMemoS(() => {
    if (filter === "all") return list;
    return list.filter((r) => r.type === filter);
  }, [list, filter]);

  // Расфасовка списка по валютам: свой блок на гривну, свой на рубли и т.д.
  // СБП попадает к рублям (это рублёвый способ), а монеты (USDT/BTC/TON) сведены
  // в один блок «Крипта» — это способы оплаты, а не отдельные валюты прайса.
  const CURRENCY_SECTIONS = [
    { id: "UAH", title: "Гривна", sign: "₴", match: (r) => r.currency === "UAH" },
    { id: "RUB", title: "Рубль", sign: "₽", match: (r) => r.currency === "RUB" },
    { id: "USD", title: "Доллар", sign: "$", match: (r) => r.currency === "USD" },
    { id: "CRYPTO", title: "Крипта", sign: "₿", match: (r) => r.type === "crypto" },
    { id: "STARS", title: "Telegram Stars", sign: "★", match: (r) => r.type === "stars" },
  ];
  const grouped = useMemoS(() => {
    const used = new Set();
    const out = [];
    CURRENCY_SECTIONS.forEach((s) => {
      const rows = filtered.filter((r) => !used.has(r.id) && s.match(r));
      rows.forEach((r) => used.add(r.id));
      if (rows.length) out.push({ id: s.id, title: s.title, sign: s.sign, rows });
    });
    // Всё, что не подошло ни под один блок, показываем отдельно — чтобы реквизит
    // не пропал из вкладки молча.
    const rest = filtered.filter((r) => !used.has(r.id));
    if (rest.length) out.push({ id: "OTHER", title: "Прочее", sign: "•", rows: rest });
    return out;
  }, [filtered]);

  const current = list.find((r) => r.id === selected);

  const totalActive = list.filter((r) => r.active).length;
  const totalPaid = list.reduce((s, r) => s + r.paid7d, 0);
  const topMethod = list.reduce(
    (acc, r) => (r.sum7d > (acc?.sum7d || 0) ? r : acc),
    null,
  );

  const toggleActive = (id) => {
    if (!editable) return;  // группа на общих реквизитах — только просмотр
    setList(list.map((r) => (r.id === id ? { ...r, active: !r.active } : r)));
    setDirty(true);
  };
  const updateCurrent = (patch) => {
    if (!editable) return;
    setList(list.map((r) => (r.id === selected ? { ...r, ...patch } : r)));
    setDirty(true);
  };

  return (
    <div className="space-y-6">
      <SectionTitle title="Реквизиты" subtitle="Карты, банки и крипто-кошельки для приёма оплат">
        <button
          onClick={reload}
          className="px-3 py-2 rounded-lg bg-white/50 border border-black/10 hover:bg-white/70 text-zinc-100 text-sm flex items-center gap-1.5"
          title="Перечитать с сервера (сбросит несохранённые изменения!)"
        >
          <Icon name="refresh" className="w-3.5 h-3.5" /> Обновить
        </button>
        {/* «Применить ко всем» убрано: реквизиты теперь глобальные (единый источник
            defaults), правка применяется ко всем аккаунтам автоматически. */}
        {/* Dirty-dot: визуальный маркер несохранённых изменений */}
        {editable && (
          <div className="relative">
            {dirty && (
              <span className="absolute -top-0.5 -right-0.5 w-2.5 h-2.5 rounded-full bg-amber-400 ring-2 ring-white pointer-events-none" title="Есть несохранённые изменения" />
            )}
            <SaveButton
              onSave={async () => {
                await savePayments(group, list);
                // После save — refetch чтобы paid7d/sum7d стали свежими
                await reload();
              }}
              label={dirty ? "Сохранить все •" : "Сохранить все"}
            />
          </div>
        )}
      </SectionTitle>

      {groups.length > 1 && (
        <div className="flex items-center gap-2 flex-wrap">
          {groups.map((g) => {
            const on = g.id === group;
            return (
              <button
                key={g.id || "common"}
                onClick={() => { setGroup(g.id); setSelected(null); }}
                className={"flex items-center gap-2 px-3.5 py-2 rounded-xl border text-sm transition-colors " +
                  (on
                    ? "bg-[rgba(139,195,74,0.16)] border-[rgba(139,195,74,0.55)] text-emerald-100 font-medium"
                    : "bg-white/40 border-black/10 text-zinc-300 hover:bg-white/60")}
              >
                <Icon name="users" className="w-3.5 h-3.5" />
                <span className="truncate max-w-[170px]">{g.name}</span>
                <span className={"text-[11px] rounded-full px-2 py-0.5 " + (on ? "bg-[rgba(139,195,74,0.3)] text-emerald-100" : "bg-black/10 text-zinc-400")}>{g.count}</span>
                {g.id && g.own && <span className="w-1.5 h-1.5 rounded-full bg-emerald-400" title="свои реквизиты" />}
              </button>
            );
          })}
          <span className="text-[11px] text-zinc-500 ml-auto">группы = владельцы из «Аккаунтов»</span>
        </div>
      )}

      {group && !activeGroup.own && (
        <div className="flex items-center gap-3 bg-white/40 border border-black/10 rounded-xl px-4 py-3">
          <Icon name="alert" className="w-4 h-4 text-zinc-400 shrink-0" />
          <div className="flex-1 text-sm text-zinc-300">
            Аккаунты «{activeGroup.name}» ({activeGroup.count}) принимают оплату на <b>общие реквизиты</b>. Ниже — их копия для примера, редактирование выключено.
          </div>
          <button
            onClick={() => onToggleOwn(true)}
            className="shrink-0 px-3.5 py-2 rounded-lg bg-[#82b93f] hover:bg-[#76a838] text-white text-sm font-medium"
          >
            Задать свои реквизиты
          </button>
        </div>
      )}
      {group && !!activeGroup.own && (
        <div className="flex items-center gap-3 bg-[rgba(139,195,74,0.12)] border border-[rgba(139,195,74,0.35)] rounded-xl px-4 py-3">
          <Icon name="check" className="w-4 h-4 text-emerald-400 shrink-0" />
          <div className="flex-1 text-sm text-zinc-200">
            «{activeGroup.name}» ({activeGroup.count} акк.) принимают на <b>свои</b> реквизиты. Правь карты ниже — общие не затронутся.
          </div>
          <button
            onClick={() => onToggleOwn(false)}
            className="shrink-0 px-3 py-2 rounded-lg bg-white/50 border border-black/10 hover:bg-white/70 text-zinc-100 text-sm"
          >
            Вернуть общие
          </button>
        </div>
      )}

      <div className="grid grid-cols-4 gap-4">
        <Metric label="Активны" value={totalActive} sublabel={"из " + list.length + " реквизитов"} />
        <Metric label="Оплат за 7 дней" value={totalPaid} sublabel="через все реквизиты" />
        <Card className="p-5">
          <div className="text-xs text-zinc-500 font-medium uppercase tracking-wider">Топ-метод</div>
          <div className="text-3xl font-semibold tracking-tight mt-2 truncate">{topMethod ? topMethod.bank : "—"}</div>
          <div className="text-[11px] text-zinc-500 mt-1">
            {topMethod ? `${topMethod.paid7d} оплат · ${topMethod.sum7d.toLocaleString("ru")} ${topMethod.currency}` : "нет данных"}
          </div>
        </Card>
        <Card className="p-5">
          <div className="text-xs text-zinc-500 font-medium uppercase tracking-wider">Скрыты</div>
          <div className="text-3xl font-semibold tracking-tight mt-2">{list.length - totalActive}</div>
          <div className="text-[11px] text-zinc-500 mt-1">не используются ботом</div>
        </Card>
      </div>

      <div className="grid grid-cols-12 gap-4">
        <Card className="col-span-7 flex flex-col overflow-hidden">
          <div className="px-4 py-3 border-b border-black/[0.06] flex items-center justify-between shrink-0">
            <div className="text-sm font-semibold">Все реквизиты</div>
            <SegmentedControl
              value={filter}
              onChange={setFilter}
              size="sm"
              options={[
                { id: "all", label: "Все" },
                { id: "card", label: "Карты" },
                { id: "crypto", label: "Крипто" },
                { id: "stars", label: "Stars" },
              ]}
            />
          </div>

          <div className="divide-y divide-black/[0.06]">
            {loading && Array.from({ length: 5 }).map((_, i) => (
              <div key={"sk" + i} className="px-4 py-3.5 flex items-center gap-4">
                <div className="w-10 h-10 rounded-xl bg-zinc-800/60 animate-pulse" />
                <div className="flex-1 space-y-1.5">
                  <div className="h-3 bg-zinc-800/60 rounded animate-pulse w-1/3" />
                  <div className="h-2 bg-zinc-800/40 rounded animate-pulse w-1/2" />
                </div>
                <div className="h-8 w-12 bg-zinc-800/40 rounded animate-pulse" />
              </div>
            ))}
            {!loading && filtered.length === 0 && (
              <div className="p-8 text-center text-sm text-zinc-500">
                Реквизиты не настроены для этого аккаунта
              </div>
            )}
            {!loading && grouped.map((section) => (
              <React.Fragment key={section.id}>
                <div className="px-4 py-2 flex items-center gap-2 bg-black/[0.04] text-[12px] font-medium text-zinc-300">
                  <span className="text-zinc-500">{section.sign}</span>
                  {section.title}
                  <span className="text-zinc-500 font-normal">· {section.rows.length}</span>
                </div>
                {section.rows.map((r) => {
                  const isActive = selected === r.id;
                  const meta = TYPE_META[r.type] || TYPE_META.card;
                  return (
                    <div
                      key={r.id}
                      onClick={() => setSelected(r.id)}
                      className={"px-4 py-3.5 flex items-center gap-4 cursor-pointer transition-colors " + (isActive ? "bg-[rgba(231,220,196,0.55)]" : "hover:bg-black/[0.03]") + (!r.active ? " opacity-60" : "")}
                    >
                      <div className={"w-10 h-10 rounded-xl bg-gradient-to-br " + meta.color + " flex items-center justify-center shadow-md shrink-0"}>
                        <Icon name={meta.icon} className="w-4 h-4 text-white" />
                      </div>
                      <div className="flex-1 min-w-0">
                        <div className="flex items-center gap-2 mb-0.5">
                          <div className="text-sm font-medium">{r.bank}</div>
                          <Pill tone="zinc">{r.currency}</Pill>
                          {!r.active && <Pill tone="zinc">скрыт</Pill>}
                        </div>
                        <div className="font-mono text-[11px] text-zinc-500 truncate">{r.number}</div>
                      </div>
                      <div className="text-right shrink-0">
                        <div className="text-sm font-semibold tabular-nums">{r.paid7d}</div>
                        <div className="text-[10px] text-zinc-500">7 дней</div>
                      </div>
                      <div onClick={(e) => e.stopPropagation()}>
                        <Toggle on={r.active} onChange={() => toggleActive(r.id)} />
                      </div>
                    </div>
                  );
                })}
              </React.Fragment>
            ))}
          </div>
        </Card>

        <Card className="col-span-5">
          {!current && !loading && <EmptyState icon="card" title="Выбери реквизит" hint="Нажми на карту/USDT/Stars слева чтобы увидеть детали." />}
          {current && (
            <div className="p-5">
              <div className="flex items-start gap-4 mb-5">
                <div className={"w-14 h-14 rounded-2xl bg-gradient-to-br " + (TYPE_META[current.type] || TYPE_META.card).color + " flex items-center justify-center shadow-lg shrink-0"}>
                  <Icon name={(TYPE_META[current.type] || TYPE_META.card).icon} className="w-6 h-6 text-white" />
                </div>
                <div className="flex-1 min-w-0">
                  <div className="text-xl font-semibold">{current.bank}</div>
                  <div className="text-sm text-zinc-500">{(TYPE_META[current.type] || TYPE_META.card).label} · {current.currency}</div>
                </div>
                <Toggle on={current.active} onChange={() => toggleActive(current.id)} />
              </div>

              {/* Visual card */}
              <div className="rounded-2xl p-5 mb-5 relative overflow-hidden text-white shadow-xl"
                   style={{ background: "linear-gradient(135deg, #6f5a4a 0%, #463527 55%, #2a2620 100%)" }}>
                <div className="absolute -right-10 -top-10 w-40 h-40 rounded-full bg-white/5" />
                <div className="absolute -right-20 -bottom-20 w-48 h-48 rounded-full bg-white/5" />
                <div className="relative">
                  <div className="flex items-center justify-between mb-8">
                    <div className="text-[11px] uppercase tracking-wider opacity-70">{current.bank}</div>
                    <Icon name={(TYPE_META[current.type] || TYPE_META.card).icon} className="w-7 h-7 opacity-80" />
                  </div>
                  <div className="font-mono text-lg tracking-wider mb-4 break-all">{current.number || "—"}</div>
                  <div className="flex items-end justify-between">
                    <div>
                      <div className="text-[10px] uppercase tracking-wider opacity-60 mb-0.5">Метка</div>
                      <div className="text-sm">{current.label || "—"}</div>
                    </div>
                    <div>
                      <div className="text-[10px] uppercase tracking-wider opacity-60 mb-0.5">Валюта</div>
                      <div className="text-sm font-semibold">{current.currency}</div>
                    </div>
                  </div>
                </div>
              </div>

              <div className="grid grid-cols-2 gap-3 mb-5">
                <div className="bg-white/40 border border-black/[0.06] rounded-xl p-3">
                  <div className="text-[10px] uppercase tracking-wider text-zinc-500 font-semibold">Оплат за 7 дней</div>
                  <div className="text-2xl font-semibold tabular-nums mt-1">{current.paid7d}</div>
                </div>
                <div className="bg-white/40 border border-black/[0.06] rounded-xl p-3">
                  <div className="text-[10px] uppercase tracking-wider text-zinc-500 font-semibold">Сумма за 7 дней</div>
                  <div className="text-2xl font-semibold tabular-nums mt-1">
                    {current.sum7d.toLocaleString("ru")} <span className="text-zinc-500 text-base">{current.currency === "RUB" ? "₽" : current.currency === "UAH" ? "₴" : current.currency === "USDT" ? "$" : "⭐"}</span>
                  </div>
                </div>
              </div>

              <div className="space-y-3">
                <Field label="Банк / тип" value={current.bank} onChange={(e) => updateCurrent({ bank: e.target.value })} />
                <Field label="Номер / адрес" value={current.number} mono onChange={(e) => updateCurrent({ number: e.target.value })} />
                <Field label="Метка" value={current.label || ""} onChange={(e) => updateCurrent({ label: e.target.value })} />

                {/* Прогрев — только для банковских карт (грн/руб). Крипта/PayPal/
                    Stars не греются. Карта наращивает оборот аккуратно: не больше
                    N чеков в сутки + пауза после каждого, иначе бот её не выдаёт. */}
                {current.type === "card" && (
                  <div className="rounded-xl bg-white/40 border border-black/[0.06] p-3 space-y-2.5">
                    <label className="flex items-center gap-2.5 cursor-pointer select-none">
                      <input
                        type="checkbox"
                        checked={!!current.warmup}
                        onChange={(e) => updateCurrent({ warmup: e.target.checked })}
                        className="w-4 h-4 accent-[#82b93f]"
                      />
                      <span className="text-sm font-medium">🔥 Прогрев карты</span>
                    </label>
                    {current.warmup && (
                      <>
                        <div className="grid grid-cols-2 gap-3">
                          <div>
                            <div className="text-[11px] uppercase tracking-wider text-zinc-500 font-semibold mb-1">Чеков в сутки (макс)</div>
                            <input
                              type="number" min="1" step="1"
                              value={current.warmupDailyLimit ?? 3}
                              onChange={(e) => updateCurrent({ warmupDailyLimit: Math.max(1, parseInt(e.target.value) || 1) })}
                              className="w-full px-3 py-2 rounded-lg bg-white/50 border border-black/10 text-sm text-zinc-100 outline-none focus:border-[rgba(139,195,74,0.6)]"
                            />
                          </div>
                          <div>
                            <div className="text-[11px] uppercase tracking-wider text-zinc-500 font-semibold mb-1">Пауза, часов</div>
                            <input
                              type="number" min="0" step="0.5"
                              value={current.warmupPauseHours ?? 2}
                              onChange={(e) => updateCurrent({ warmupPauseHours: Math.max(0, parseFloat(e.target.value) || 0) })}
                              className="w-full px-3 py-2 rounded-lg bg-white/50 border border-black/10 text-sm text-zinc-100 outline-none focus:border-[rgba(139,195,74,0.6)]"
                            />
                          </div>
                        </div>
                        <div className="text-[12px] text-zinc-400 leading-relaxed">
                          Бот выдаёт эту карту не чаще {current.warmupDailyLimit ?? 3} раз в сутки, и после каждого присланного чека делает паузу {current.warmupPauseHours ?? 2} ч. Пока карта «остывает» — бот берёт другую карту той же валюты, а если других нет — уводит на другой способ.
                        </div>
                      </>
                    )}
                  </div>
                )}
              </div>

              <div className="flex items-center justify-end gap-2 mt-5 pt-5 border-t border-black/[0.06]">
                {/* «Скопировать», «Удалить», нижняя «Сохранить» раньше были
                    без onClick = silent fail. «Сохранить» особенно сбивал с
                    толку (есть верхняя «Сохранить все» в SectionTitle которая
                    реально шлёт savePayments). Оставлена только реально
                    работающая Copy, остальные disabled с tooltip'ом. */}
                <div className="text-[11px] text-zinc-500 mr-auto">
                  Изменения сохраняются через «Сохранить все» в шапке страницы
                </div>
                <button
                  onClick={() => { try { navigator.clipboard.writeText(current.number || ""); } catch {} }}
                  className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-white/50 border border-black/10 hover:bg-white/70 text-zinc-100 text-sm"
                  title="Копировать номер/адрес в буфер обмена"
                >
                  <Icon name="copy" className="w-3.5 h-3.5" /> Скопировать
                </button>
                <button
                  disabled
                  title="Удаление реквизита через UI пока не реализовано. Чтобы убрать — выключи toggle слева и нажми «Сохранить все»."
                  className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-black/[0.02] border border-black/[0.06] text-zinc-500 text-sm cursor-not-allowed"
                >
                  <Icon name="trash" className="w-3.5 h-3.5" /> Удалить
                </button>
              </div>
            </div>
          )}
        </Card>
      </div>
    </div>
  );
}

/* ============================
   CONTENT — NEW
============================ */

function ChannelDeliveryEditor() {
  // «Текст выдачи после оплаты» (private_channel_text). Таргетит базовые
  // настройки (в этой панели нет per-account селектора). Отправляется клиенту
  // КАК ЕСТЬ после оплаты канала (Phase 3).
  const [text, setText] = useStateS("");
  const [loaded, setLoaded] = useStateS(false);
  const [saving, setSaving] = useStateS(false);
  const [msg, setMsg] = useStateS("");
  useEffectS(() => {
    let stop = false;
    (async () => {
      const r = await fetchChannelText(null);
      if (!stop) { setText((r && r.text) || ""); setLoaded(true); }
    })();
    return () => { stop = true; };
  }, []);
  const save = async () => {
    setSaving(true); setMsg("");
    try {
      await saveChannelText(null, text);
      setMsg("Сохранено");
    } catch (e) {
      setMsg("Ошибка: " + (e && e.message ? e.message : e));
    } finally {
      setSaving(false);
      setTimeout(() => setMsg(""), 4000);
    }
  };
  return (
    <div className="mb-3 rounded-lg border border-zinc-700/60 bg-zinc-800/40 p-3">
      <div className="text-sm font-medium text-zinc-200 mb-1">Текст выдачи после оплаты</div>
      <div className="text-[11px] text-zinc-500 mb-2">
        Отправляется клиенту как есть после оплаты канала: ссылка, несколько ссылок,
        инструкция, пароль, правила — любой текст. Пусто — продажа разрешена, доступ
        бот честно отложит.
      </div>
      <textarea
        value={text}
        onChange={(e) => setText(e.target.value)}
        rows={8}
        placeholder={"Вот ссылка на канал:\nhttps://t.me/+xxxxx"}
        disabled={!loaded}
        className="w-full bg-zinc-900/60 border border-zinc-700/60 rounded-lg px-3 py-2 text-sm text-zinc-200 placeholder:text-zinc-600 focus:outline-none focus:border-indigo-500/50 font-mono"
      />
      <div className="flex items-center gap-2 mt-2">
        <button onClick={save} disabled={saving || !loaded}
          className="px-3.5 py-2 rounded-lg bg-indigo-500 hover:bg-indigo-600 text-white text-sm font-medium disabled:opacity-50">
          {saving ? "Сохранение…" : "Сохранить"}
        </button>
        {msg && <span className="text-xs text-zinc-400">{msg}</span>}
      </div>
    </div>
  );
}

function ContentPage() {
  const [items, setItems] = useStateS([]);
  const [loading, setLoading] = useStateS(true);
  const [category, setCategory] = useStateS("all");
  const [selected, setSelected] = useStateS(null);
  const [search, setSearch] = useStateS("");
  const [reindexing, setReindexing] = useStateS(false);
  const [reindexMsg, setReindexMsg] = useStateS("");

  // Polling 30 сек. При смене категории — перезагружаем с правильным фильтром.
  useEffectS(() => {
    let stopped = false;
    const refresh = async () => {
      const data = await fetchContent({ category, limit: 500 });
      if (stopped) return;
      setItems(data);
      setLoading(false);
      setSelected((cur) => cur || (data[0] ? data[0].id : null));
    };
    refresh();
    const id = setInterval(refresh, 30_000);
    return () => { stopped = true; clearInterval(id); };
  }, [category]);

  const filtered = useMemoS(() => {
    if (!search) return items;
    const q = search.toLowerCase();
    return items.filter((m) =>
      (m.summary || "").toLowerCase().includes(q) ||
      (m.summarySales || "").toLowerCase().includes(q) ||
      (m.tags || []).some((t) => t.toLowerCase().includes(q)) ||
      m.name.toLowerCase().includes(q)
    );
  }, [search, items]);

  const current = items.find((c) => c.id === selected);

  // Подсчитываем количества по категориям для sidebar (только из текущей выборки).
  const categoryCounts = useMemoS(() => {
    const counts = { all: items.length, photo: 0, video: 0, circle: 0, channel: 0 };
    for (const m of items) {
      if (m.type === "photo") counts.photo += 1;
      else if (m.type === "video") counts.video += 1;
      if (m.category === "circle" || m.category === "custom_circle") counts.circle += 1;
      if (m.category === "channel") counts.channel += 1;
    }
    return counts;
  }, [items]);

  // Индексированных / без AI — для блока внизу sidebar
  const aiStats = useMemoS(() => {
    let indexed = 0, missing = 0, errored = 0;
    for (const m of items) {
      if (m.aiStatus === "indexed") indexed += 1;
      else if (m.aiStatus === "error") errored += 1;
      else missing += 1;
    }
    return { indexed, missing, errored };
  }, [items]);

  const handleReindex = async () => {
    if (reindexing) return;
    if (!window.confirm("Запустить AI-переиндексацию ВСЕГО контента? Опишет все фото и видео заново под текущую модель (несколько минут, в фоне).")) return;
    setReindexing(true);
    setReindexMsg("запуск…");
    try {
      const r = await reindexContent(true);
      if (r && r.alreadyRunning) setReindexMsg("уже идёт…");
      const poll = async () => {
        const st = await reindexStatus();
        if (st.phase === "error") {
          setReindexMsg("ошибка: " + (st.error || ""));
          setReindexing(false);
        } else if (st.running || st.phase === "running") {
          setReindexMsg(`описано ${st.done || 0}/${st.total || "?"}${st.errors ? ` (ошибок ${st.errors})` : ""}`);
          setTimeout(poll, 2000);
        } else {
          setReindexMsg(st.phase === "done"
            ? `готово: ${st.indexed || 0} описано${st.errors ? `, ${st.errors} ошибок` : ""}`
            : "готово");
          setReindexing(false);
          try { setItems(await fetchContent({ category, limit: 500 })); } catch (e) {}
          setTimeout(() => setReindexMsg(""), 8000);
        }
      };
      setTimeout(poll, 2500);
    } catch (e) {
      setReindexMsg("ошибка: " + (e?.message || e));
      setReindexing(false);
    }
  };

  return (
    <div className="space-y-6">
      <SectionTitle title="Контент" subtitle="Фото, видео и кружки с AI-описаниями для подбора под клиента">

        {/* «Применить ко всем» убрано: контент глобальный (общий мастер
            admin_panel/content), доступен всем аккаунтам автоматически. */}
        {reindexMsg && <span className="text-xs text-zinc-400 mr-1">{reindexMsg}</span>}
        <button onClick={handleReindex} disabled={reindexing} className="flex items-center gap-2 px-3.5 py-2 rounded-lg bg-zinc-800/60 border border-zinc-700/60 hover:bg-zinc-800 text-zinc-300 text-sm disabled:opacity-60">
          <Icon name="sparkles" className="w-3.5 h-3.5" /> {reindexing ? "Индексация…" : "Переиндексировать AI"}
        </button>
        <button className="flex items-center gap-2 px-3.5 py-2 rounded-lg bg-indigo-500 hover:bg-indigo-600 text-white text-sm font-medium transition-colors">
          <Icon name="upload" className="w-3.5 h-3.5" /> Загрузить
        </button>
      </SectionTitle>

      <div className="grid grid-cols-12 gap-4">
        {/* sidebar */}
        <Card className="col-span-3 p-3 flex flex-col" style={{ height: "calc(100vh - 220px)", minHeight: 540 }}>
          <div className="px-2 pt-1 pb-3">
            <div className="text-[10px] uppercase tracking-wider text-zinc-500 font-semibold mb-2 px-1">Категории</div>
            <div className="space-y-1">
              {CONTENT_CATEGORIES.map((c) => {
                const isActive = category === c.id;
                const count = categoryCounts[c.id] || 0;
                return (
                  <button
                    key={c.id}
                    onClick={() => setCategory(c.id)}
                    className={"w-full flex items-center justify-between px-3 py-2 rounded-lg text-sm transition-colors " + (isActive ? "bg-[rgba(27,25,21,.06)] text-zinc-50 font-medium" : "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50")}
                  >
                    <span className="flex items-center gap-2">
                      <Icon
                        name={c.id === "video" ? "video" : c.id === "circle" ? "camera" : c.id === "channel" ? "lock" : c.id === "photo" ? "image" : "hash"}
                        className="w-3.5 h-3.5"
                      />
                      {c.label}
                    </span>
                    <span className="text-[11px] text-zinc-500 tabular-nums">{count}</span>
                  </button>
                );
              })}
            </div>
          </div>

          <div className="px-2 pt-3 border-t border-zinc-800/60 mt-2">
            <div className="text-[10px] uppercase tracking-wider text-zinc-500 font-semibold mb-2 px-1">AI теги</div>
            <div className="flex flex-wrap gap-1.5 px-1">
              {CONTENT_TAGS.map((t) => (
                <button
                  key={t}
                  onClick={() => setSearch(search === t ? "" : t)}
                  className={"px-2 py-1 rounded-full text-[11px] border transition-colors " + (search === t ? "bg-indigo-500/15 border-indigo-500/30 text-indigo-300" : "bg-zinc-800/40 border-zinc-700/60 text-zinc-400 hover:text-zinc-200")}
                >
                  #{t}
                </button>
              ))}
            </div>
          </div>

          <div className="px-2 mt-auto pt-3 border-t border-zinc-800/60 text-[11px] text-zinc-500">
            <div className="flex items-center gap-2 px-1 py-1.5">
              <Icon name="sparkles" className="w-3.5 h-3.5 text-indigo-400" />
              <span>AI описал <span className="text-zinc-300 font-medium">{aiStats.indexed}</span> файлов</span>
            </div>
            <div className="flex items-center gap-2 px-1 py-1.5">
              <Icon name="alert" className="w-3.5 h-3.5 text-amber-400" />
              <span>Без AI: <span className="text-zinc-300 font-medium">{aiStats.missing}</span></span>
            </div>
            {aiStats.errored > 0 && (
              <div className="flex items-center gap-2 px-1 py-1.5">
                <Icon name="alert" className="w-3.5 h-3.5 text-rose-400" />
                <span>Ошибок AI: <span className="text-zinc-300 font-medium">{aiStats.errored}</span></span>
              </div>
            )}
          </div>
        </Card>

        {/* gallery */}
        <Card className="col-span-6 flex flex-col overflow-hidden" style={{ height: "calc(100vh - 220px)", minHeight: 540 }}>
          <div className="p-3 border-b border-zinc-800/80 flex items-center gap-2 shrink-0">
            <div className="relative flex-1">
              <Icon name="search" className="w-4 h-4 text-zinc-500 absolute left-3 top-1/2 -translate-y-1/2" />
              <input
                value={search}
                onChange={(e) => setSearch(e.target.value)}
                placeholder="Поиск по описанию или тегу…"
                className="w-full bg-zinc-800/60 border border-zinc-700/60 rounded-lg pl-9 pr-3 py-2 text-sm placeholder:text-zinc-500 focus:outline-none focus:border-indigo-500/50"
              />
            </div>
            <div className="text-[11px] text-zinc-500 px-2">
              {filtered.length} файлов
            </div>
          </div>

          <div className="flex-1 overflow-y-auto p-4">
            {category === "channel" && <ChannelDeliveryEditor />}
            <div className="grid grid-cols-3 gap-2.5">
              {loading && Array.from({ length: 12 }).map((_, i) => (
                <div key={"sk" + i} className="aspect-square rounded-lg bg-zinc-800/60 animate-pulse ring-1 ring-zinc-800" />
              ))}
              {!loading && filtered.map((m) => (
                <ContentTile key={m.id} item={m} active={selected === m.id} onClick={() => setSelected(m.id)} />
              ))}
              {!loading && filtered.length === 0 && (
                <div className="col-span-3">
                  <EmptyState icon="image" title="Контента нет" hint="Загрузи файлы в этом аккаунте или смени категорию." />
                </div>
              )}
            </div>
          </div>
        </Card>

        {/* detail */}
        <Card className="col-span-3 flex flex-col overflow-hidden" style={{ height: "calc(100vh - 220px)", minHeight: 540 }}>
          {current ? <ContentDetail item={current} /> : <EmptyState icon="image" title="Файл не выбран" />}
        </Card>
      </div>
    </div>
  );
}

function ContentTile({ item, active, onClick }) {
  // Реальная миниатюра из /api/content/thumb. Для фото — оригинал; для видео —
  // кеш из panel_data/_thumbs (если есть). При ошибке загрузки показываем
  // gradient-плейсхолдер.
  const [thumbFailed, setThumbFailed] = useStateS(false);
  const isVideo = item.type === "video";
  const isCircle = item.category === "circle" || item.category === "custom_circle" || item.category === "virt_circle" || item.category === "proof_circle";
  const iconName = isVideo ? "video" : isCircle ? "camera" : "image";
  const typeBadge = isVideo ? "видео" : isCircle ? "кружок" : "фото";
  const confidencePct = Math.round((item.confidence || 0) * 100);
  const firstTag = (item.tags || [])[0] || item.category || "—";

  return (
    <button
      onClick={onClick}
      className={"relative aspect-square rounded-xl overflow-hidden ring-1 transition-all group " + (active ? "ring-2 ring-[#26231d] scale-[0.98]" : "ring-zinc-800 hover:ring-zinc-700")}
    >
      {thumbFailed ? (
        <>
          <div className="absolute inset-0 bg-gradient-to-br from-zinc-700 to-zinc-900" style={{ filter: "saturate(.7) brightness(.7)" }} />
          <div className="absolute inset-0 flex items-center justify-center">
            <Icon name={iconName} className="w-8 h-8 text-zinc-500" />
          </div>
        </>
      ) : (
        <img
          src={contentThumbUrl(item.key)}
          alt={item.name}
          loading="lazy"
          onError={() => setThumbFailed(true)}
          className="absolute inset-0 w-full h-full object-cover"
        />
      )}
      {/* Ховер-градиент для подписи */}
      <div className="absolute inset-0 bg-gradient-to-t from-black/55 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />

      {/* Тип — тихий глиф снизу-слева */}
      <div className="absolute bottom-2 left-2 text-white/85" style={{ filter: "drop-shadow(0 1px 2px rgba(0,0,0,.55))" }}>
        <Icon name={iconName} className="w-3.5 h-3.5" />
      </div>

      {/* AI-статус — тихая точка сверху-справа (проценты в тултипе) */}
      <div className="absolute top-2 right-2">
        {item.aiStatus === "indexed" ? (
          <span className="block w-2.5 h-2.5 rounded-full bg-emerald-400" style={{ boxShadow: "0 0 0 2px rgba(0,0,0,.15)" }} title={confidencePct + "% AI"} />
        ) : item.aiStatus === "error" ? (
          <span className="block w-2.5 h-2.5 rounded-full bg-rose-400" style={{ boxShadow: "0 0 0 2px rgba(0,0,0,.15)" }} title="ошибка AI" />
        ) : (
          <span className="block w-2.5 h-2.5 rounded-full border-2 border-amber-400" title="без AI" />
        )}
      </div>

      {/* Подпись (тег) — только по наведению */}
      <div className="absolute bottom-2 left-8 right-2 text-[10px] text-white/90 truncate opacity-0 group-hover:opacity-100 transition-opacity">
        {firstTag}
      </div>
    </button>
  );
}

function ContentDetail({ item }) {
  const isVideo = item.type === "video";
  const sizeMB = item.sizeBytes ? (item.sizeBytes / (1024 * 1024)).toFixed(1) : null;

  return (
    <>
      <div className="p-4 border-b border-zinc-800/80 flex items-center justify-between shrink-0">
        <div className="min-w-0">
          <div className="text-sm font-semibold truncate" title={item.name}>{item.name}</div>
          <div className="text-[11px] text-zinc-500 mt-0.5">
            {item.category}{sizeMB ? " · " + sizeMB + " МБ" : ""}
          </div>
        </div>
        <a
          href={contentFileUrl(item.key)}
          target="_blank"
          rel="noreferrer"
          className="w-8 h-8 rounded-md flex items-center justify-center text-zinc-500 hover:text-zinc-200 hover:bg-zinc-800"
          title="Открыть оригинал"
        >
          <Icon name="more" className="w-4 h-4" />
        </a>
      </div>

      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {/* Реальное превью / видео */}
        <div className="rounded-xl bg-zinc-950 ring-1 ring-zinc-800 overflow-hidden flex items-center justify-center" style={{ minHeight: 240 }}>
          {isVideo ? (
            <video
              key={item.key}
              src={contentFileUrl(item.key)}
              controls
              preload="metadata"
              className="max-w-full max-h-[360px]"
            />
          ) : (
            <img
              src={contentFileUrl(item.key)}
              alt={item.name}
              className="max-w-full max-h-[360px] object-contain"
              loading="lazy"
            />
          )}
        </div>

        {/* AI описание */}
        <div>
          <div className="text-[10px] uppercase tracking-wider text-zinc-500 font-semibold mb-2 flex items-center gap-1.5">
            <Icon name="sparkles" className="w-3 h-3 text-indigo-400" /> AI описание
          </div>
          <div className="bg-zinc-900/40 border border-zinc-800/60 rounded-xl p-3 space-y-2">
            {item.aiStatus === "indexed" ? (
              <>
                {item.summarySales && (
                  <div className="text-sm text-zinc-200 leading-relaxed">{item.summarySales}</div>
                )}
                {item.summary && (
                  <div className="text-[12px] text-zinc-400 leading-snug">{item.summary}</div>
                )}
                <ConfidenceBar value={item.confidence} />
              </>
            ) : item.aiStatus === "error" ? (
              <div className="text-sm text-rose-400">AI распознавание упало. Прогони ещё раз через desktop-панель.</div>
            ) : (
              <div className="text-sm text-zinc-500">AI ещё не описал этот файл. Открой в desktop-панели и нажми «AI: без описания».</div>
            )}
          </div>
        </div>

        {/* Сцены (если есть) */}
        {item.sceneBeats && item.sceneBeats.length > 0 && (
          <div>
            <div className="text-[10px] uppercase tracking-wider text-zinc-500 font-semibold mb-2">Сцены по порядку</div>
            <ul className="text-[12px] text-zinc-300 space-y-1 list-disc pl-4">
              {item.sceneBeats.map((s, i) => <li key={i}>{s}</li>)}
            </ul>
          </div>
        )}

        {/* Теги */}
        <div>
          <div className="text-[10px] uppercase tracking-wider text-zinc-500 font-semibold mb-2">Теги</div>
          <div className="flex flex-wrap gap-1.5">
            {(item.tags || []).map((t, i) => <Pill key={i} tone="indigo">#{t}</Pill>)}
            <Pill tone="zinc">#{item.type}</Pill>
            <Pill tone="zinc">#{item.category}</Pill>
          </div>
        </div>

        {/* Body focus + face — детали AI */}
        {(item.faceVisible || (item.bodyFocus && item.bodyFocus.length > 0)) && (
          <div className="grid grid-cols-2 gap-3 text-xs">
            <div>
              <div className="text-zinc-500 mb-0.5">Лицо</div>
              <div className="font-medium">{item.faceVisible || "—"}</div>
            </div>
            <div>
              <div className="text-zinc-500 mb-0.5">Фокус тела</div>
              <div className="font-medium">{(item.bodyFocus || []).join(", ") || "—"}</div>
            </div>
          </div>
        )}
      </div>

      <div className="p-3 border-t border-zinc-800/80 flex gap-2 shrink-0">
        <a
          href={contentFileUrl(item.key)}
          target="_blank"
          rel="noreferrer"
          download={item.name}
          className="flex-1 flex items-center justify-center gap-1.5 px-3 py-2 rounded-lg bg-zinc-800/60 border border-zinc-700/60 hover:bg-zinc-800 text-zinc-300 text-sm"
        >
          <Icon name="download" className="w-3.5 h-3.5" /> Скачать
        </a>
      </div>
    </>
  );
}

/* ============================
   SETTINGS — NEW
============================ */

function SettingsPage() {
  const [section, setSection] = useStateS("bot");
  const [data, setData] = useStateS(null);
  const [loading, setLoading] = useStateS(true);

  useEffectS(() => {
    let stopped = false;
    fetchSettings().then((d) => {
      if (stopped) return;
      setData(d);
      setLoading(false);
    });
    return () => { stopped = true; };
  }, []);

  // Read-only во всех секциях — реальные значения из .env. Запись будет в фазе B.
  const bot = data?.bot || {};
  const llm = data?.llm || {};
  const contentAi = data?.contentAnalysis || {};
  const receipts = data?.receipts || {};
  const nudges = data?.nudges || {};
  const voice = data?.voice || {};

  const sections = [
    { id: "processes", label: "Процессы", icon: "refresh" },
    { id: "bot", label: "Бот", icon: "zap" },
    { id: "tg", label: "Telegram API", icon: "chat" },
    { id: "llm", label: "LLM модель", icon: "sparkles" },
    { id: "voice", label: "Голос (ElevenLabs)", icon: "play" },
    { id: "checks", label: "Чеки и оплаты", icon: "receipt" },
    { id: "nudges", label: "Напоминания", icon: "bell" },
    { id: "appearance", label: "Внешний вид", icon: "image" },
    { id: "danger", label: "Опасная зона", icon: "alert" },
  ];

  return (
    <div className="space-y-6">
      <SectionTitle title="Настройки" subtitle="Переменные окружения и параметры бота (read-only — запись в фазе B)">
        {loading && <Pill tone="zinc">загружается…</Pill>}
        {!loading && (
          <Pill tone="indigo">
            <Icon name="more" className="w-3 h-3" /> {bot.envPath ? bot.envPath.split(/[\\/]/).slice(-2).join("/") : ".env"}
          </Pill>
        )}
      </SectionTitle>

      <div className="grid grid-cols-12 gap-4">
        <Card className="col-span-3 p-2">
          {sections.map((s) => {
            const isActive = section === s.id;
            return (
              <button
                key={s.id}
                onClick={() => setSection(s.id)}
                className={"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors " + (isActive ? "bg-indigo-500/15 text-indigo-300" : s.id === "danger" ? "text-rose-400/80 hover:bg-rose-500/10" : "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50")}
              >
                <Icon name={s.icon} className="w-4 h-4" />
                <span className="font-medium">{s.label}</span>
              </button>
            );
          })}
        </Card>

        <Card className="col-span-9 p-6">
          {section === "processes" && <ProcessesSection />}
          {section === "bot" && (
            <SettingsSection title="Бот" subtitle="Основные параметры бота. Read-only — для записи открой .env вручную.">
              <EnvField label="ADMIN_ID" value={bot.adminId != null ? String(bot.adminId) : "—"} mono readOnly />
              <EnvField label="V3_ADMIN_PANEL_ROOT" value={bot.panelRoot || "—"} mono readOnly />
              <EnvField label="ENV файл" value={bot.envPath || "—"} mono readOnly />
            </SettingsSection>
          )}

          {section === "tg" && (
            <SettingsSection title="Telegram API" subtitle="Токен бота и URL Bot API сервера">
              <EnvField label="TELEGRAM_TOKEN" value={bot.telegramToken || (bot.telegramTokenSet ? "***" : "не задан")} secret mono readOnly />
              <EnvField label="TELEGRAM_BOT_API_URL" value={bot.telegramBotApiUrl || "https://api.telegram.org"} mono readOnly />
              <div className="text-[12px] text-zinc-500 leading-relaxed mt-2 p-3 bg-zinc-900/40 rounded-xl border border-zinc-800/60">
                Если URL не <code className="text-zinc-300">api.telegram.org</code> — значит используется локальный telegram-bot-api Docker (лимит загрузки 2&nbsp;ГБ вместо 49&nbsp;МБ).
              </div>
            </SettingsSection>
          )}

          {section === "llm" && (
            <SettingsSection title="LLM модель" subtitle="OpenRouter + Anthropic/Qwen/Grok для диалогов и vision">
              <EnvField label="V3_GROK_MODEL" value={llm.model || "—"} mono readOnly />
              <EnvField label="XAI_API_URL" value={llm.apiUrl || "—"} mono readOnly />
              <EnvField label="XAI_API_KEY" value={llm.apiKey || (llm.apiKeySet ? "***" : "не задан")} secret mono readOnly />
              <EnvField label="V3_GROK_TIMEOUT_SECONDS" value={String(llm.timeoutSeconds || "—")} mono readOnly />
              <EnvField label="V3_GROK_MAX_TOKENS" value={String(llm.maxTokens || "—")} mono readOnly />
              <div className="text-[10px] uppercase tracking-wider text-zinc-500 font-semibold mt-6 mb-2">Vision (описание контента + чеки)</div>
              <EnvField label="CONTENT_ANALYSIS_MODEL" value={contentAi.model || "—"} mono readOnly />
              <EnvField label="CONTENT_ANALYSIS_TIMEOUT_SECONDS" value={String(contentAi.timeoutSeconds || "—")} mono readOnly />
              <EnvField label="CONTENT_ANALYSIS_MAX_TOKENS" value={String(contentAi.maxTokens || "—")} mono readOnly />
            </SettingsSection>
          )}

          {section === "voice" && (
            <SettingsSection title="Голос (ElevenLabs)" subtitle="Параметры голосовых proof-сообщений">
              <EnvField label="ELEVENLABS_API_KEY" value={voice.apiKey || (voice.apiKeySet ? "***" : "не задан")} secret mono readOnly />
              <EnvField label="ELEVENLABS_VOICE_ID" value={voice.voiceId || "—"} mono readOnly />
              <EnvField label="ELEVENLABS_MODEL_ID" value={voice.modelId || "—"} mono readOnly />
              <EnvField label="ELEVENLABS_OUTPUT_FORMAT" value={voice.outputFormat || "—"} mono readOnly />
              <EnvField label="ELEVENLABS_SPEED" value={String(voice.speed || "1.0")} mono readOnly />
              <EnvField label="V3_VOICE_CACHE_DIR" value={voice.cacheDir || "—"} mono readOnly />
              <EnvField label="PROOF_FREE_VOICE_LIMIT" value={String(voice.freeVoiceLimit || "—")} mono readOnly />
            </SettingsSection>
          )}

          {section === "checks" && (
            <SettingsSection title="Чеки и оплаты" subtitle="Тайминги auto-approve и AI-reject">
              <EnvField label="RECEIPT_REVIEW_DELAY_SECONDS" value={String(receipts.reviewDelaySeconds || "—")} mono readOnly />
              <EnvField label="RECEIPT_AUTO_APPROVE_SECONDS" value={String(receipts.autoApproveSeconds || "—")} mono readOnly />
              <EnvField label="RECEIPT_AI_REJECT_SECONDS" value={String(receipts.aiRejectSeconds || "—")} mono readOnly />
              <div className="text-[12px] text-zinc-500 leading-relaxed mt-2 p-3 bg-zinc-900/40 rounded-xl border border-zinc-800/60">
                Auto-approve срабатывает если AI verdict положительный и админ не ответил за это время.
                AI-reject — окно для админа когда vision отверг чек.
              </div>
            </SettingsSection>
          )}

          {section === "nudges" && (
            <SettingsSection title="Напоминания (nudges)" subtitle="Через сколько секунд бот пишет «ну как, определился?»">
              <EnvField label="AWAITING_PAYMENT_NUDGE_SECONDS" value={String(nudges.awaitingPaymentSeconds || "—")} mono readOnly />
              <EnvField label="POST_PURCHASE_NUDGE_DELAY_SECONDS" value={String(nudges.postPurchaseSeconds || "—")} mono readOnly />
              <EnvField label="VIRT_EXTENSION_REMINDER_SECONDS" value={String(nudges.virtExtensionSeconds || "—")} mono readOnly />
              <EnvField label="CARD_REDISPLAY_COOLDOWN_SECONDS" value={String(nudges.cardRedisplayCooldownSeconds || "—")} mono readOnly />
            </SettingsSection>
          )}

          {section === "appearance" && (
            <SettingsSection title="Внешний вид" subtitle="Тема и плотность интерфейса">
              <div className="grid grid-cols-3 gap-3 mb-4">
                <ThemeCard label="Тёмная" active />
                <ThemeCard label="Светлая" />
                <ThemeCard label="Системная" />
              </div>
              <SettingRow label="Акцентный цвет" hint="Используется для кнопок и графиков">
                <div className="flex gap-2">
                  {["bg-indigo-500", "bg-emerald-500", "bg-rose-500", "bg-amber-500", "bg-fuchsia-500"].map((c, i) => (
                    <button key={i} className={"w-7 h-7 rounded-full " + c + (i === 0 ? " ring-2 ring-white/40 ring-offset-2 ring-offset-zinc-950" : "")} />
                  ))}
                </div>
              </SettingRow>
              <SettingRow label="Плотность" hint="Регулируй размер отступов через панель Tweaks">
                <Pill tone="indigo">⌘K → Tweaks</Pill>
              </SettingRow>
            </SettingsSection>
          )}

          {section === "danger" && (
            <SettingsSection title="Опасная зона" subtitle="Необратимые операции">
              <div className="space-y-3">
                <DangerRow label="Очистить историю чеков" hint="Удалит все чеки старше 30 дней" />
                <DangerRow label="Сбросить AI индекс контента" hint="Бот переиндексирует все файлы заново. ~10 мин" />
                <DangerRow label="Удалить все данные" hint="Все аккаунты, прайсы, чеки, контент. Бэкап будет создан." critical />
              </div>
            </SettingsSection>
          )}
        </Card>
      </div>
    </div>
  );
}

function SettingsSection({ title, subtitle, children }) {
  return (
    <div>
      <div className="mb-5">
        <div className="text-base font-semibold">{title}</div>
        {subtitle && <div className="text-[12px] text-zinc-500 mt-0.5">{subtitle}</div>}
      </div>
      <div className="space-y-4">{children}</div>
    </div>
  );
}

function SettingRow({ label, hint, children }) {
  return (
    <div className="flex items-center justify-between gap-6 py-2.5 border-b border-zinc-800/60 last:border-0">
      <div className="min-w-0">
        <div className="text-sm font-medium">{label}</div>
        {hint && <div className="text-[11px] text-zinc-500 mt-0.5">{hint}</div>}
      </div>
      <div className="shrink-0">{children}</div>
    </div>
  );
}

function EnvField({ label, value, secret, mono }) {
  // value уже маскирована на бэке (xxxx***yyyy) — отображаем как есть.
  // Copy-to-clipboard работает для всех полей.
  const safeValue = value || "";
  const onCopy = () => { try { navigator.clipboard.writeText(safeValue); } catch (_) {} };
  return (
    <div className="grid grid-cols-[200px_1fr] items-center gap-4">
      <div className="text-[11px] font-mono text-zinc-500 tracking-wide truncate" title={label}>{label}</div>
      <div className="relative flex items-center gap-2">
        <input
          readOnly
          value={safeValue}
          className={"flex-1 bg-zinc-800/60 border border-zinc-700/60 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-indigo-500/50 " + (mono ? "font-mono" : "")}
        />
        {secret && (
          <div className="px-2 py-2 rounded-lg bg-zinc-800/60 border border-zinc-700/60 text-zinc-500" title="Секрет — маскирован на сервере">
            <Icon name="lock" className="w-3.5 h-3.5" />
          </div>
        )}
        <button
          onClick={onCopy}
          className="px-2 py-2 rounded-lg bg-zinc-800/60 border border-zinc-700/60 hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200"
          title="Скопировать"
        >
          <Icon name="copy" className="w-3.5 h-3.5" />
        </button>
      </div>
    </div>
  );
}

function ThemeCard({ label, active }) {
  return (
    <button className={"rounded-xl p-3 border transition-all " + (active ? "border-indigo-500 ring-2 ring-indigo-500/30 bg-indigo-500/5" : "border-zinc-800 hover:border-zinc-700")}>
      <div className="aspect-[16/10] rounded-md bg-zinc-950 border border-zinc-800 mb-2 overflow-hidden relative">
        <div className="absolute left-0 top-0 bottom-0 w-1/4 bg-zinc-900 border-r border-zinc-800" />
        <div className="absolute left-[28%] top-2 right-2 h-1.5 bg-zinc-800 rounded-sm" />
        <div className="absolute left-[28%] top-5 right-2 h-3 bg-indigo-500/30 rounded-sm" />
        <div className="absolute left-[28%] top-10 right-2 h-1 bg-zinc-800 rounded-sm" />
        <div className="absolute left-[28%] top-12 right-2 h-1 bg-zinc-800 rounded-sm" />
      </div>
      <div className="text-xs font-medium">{label}</div>
    </button>
  );
}

function DangerRow({ label, hint, critical }) {
  return (
    <div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-rose-500/[0.04] border border-rose-500/20">
      <div className="min-w-0">
        <div className="text-sm font-medium text-zinc-200">{label}</div>
        {hint && <div className="text-[11px] text-zinc-500 mt-0.5">{hint}</div>}
      </div>
      <button className={"shrink-0 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors " + (critical ? "bg-rose-500 hover:bg-rose-600 text-white" : "bg-rose-500/10 border border-rose-500/30 text-rose-400 hover:bg-rose-500/20")}>
        {critical ? "Удалить всё" : "Выполнить"}
      </button>
    </div>
  );
}

/* ===========================================================
   ProcessesSection — управление автозапуском и self-restart uvicorn.
   Размещена в Settings → Процессы (первый пункт sidebar'а).
=========================================================== */

function ProcessesSection() {
  const [autostart, setAutostart] = useStateS(null);   // null = loading
  const [savingAutostart, setSavingAutostart] = useStateS(false);
  const [nudgeDedup, setNudgeDedup] = useStateS(null);
  const [savingNudgeDedup, setSavingNudgeDedup] = useStateS(false);
  const [restartConfirm, setRestartConfirm] = useStateS(false);
  const [restarting, setRestarting] = useStateS(false);
  const [restartElapsedSec, setRestartElapsedSec] = useStateS(0);
  const [lastRestart, setLastRestart] = useStateS(null);

  // Загрузка текущих admin-настроек
  useEffectS(() => {
    let stopped = false;
    fetchAdminSettings().then((s) => {
      if (stopped) return;
      setAutostart(!!s?.autoStartBot);
      setNudgeDedup(s?.nudgeDedupEnabled !== false);  // default true
      setLastRestart(s?.lastRestart || null);
    });
    return () => { stopped = true; };
  }, []);

  const onToggleAutostart = async () => {
    if (savingAutostart || autostart === null) return;
    const next = !autostart;
    setSavingAutostart(true);
    setAutostart(next); // optimistic
    try {
      const res = await updateAdminSettings({ autoStartBot: next });
      setAutostart(!!res?.autoStartBot);
      if (window.__toast) {
        window.__toast.push({
          kind: "ok",
          text: next
            ? "Автозапуск бота включён — будет стартовать с веб-сервером"
            : "Автозапуск бота выключен",
        });
      }
    } catch (e) {
      // rollback
      setAutostart(!next);
      if (window.__toast) {
        window.__toast.push({
          kind: "err",
          text: "Не удалось сохранить: " + String(e.message || e).slice(0, 100),
        });
      }
    } finally {
      setSavingAutostart(false);
    }
  };

  const onToggleNudgeDedup = async () => {
    if (savingNudgeDedup || nudgeDedup === null) return;
    const next = !nudgeDedup;
    setSavingNudgeDedup(true);
    setNudgeDedup(next);
    try {
      const res = await updateAdminSettings({ nudgeDedupEnabled: next });
      setNudgeDedup(res?.nudgeDedupEnabled !== false);
      if (window.__toast) {
        window.__toast.push({
          kind: "ok",
          text: next
            ? "Anti-repetition включён — guard блокирует повторы nudge-фраз"
            : "Anti-repetition выключен — guard пропускает все nudges как раньше",
        });
      }
    } catch (e) {
      setNudgeDedup(!next);
      if (window.__toast) {
        window.__toast.push({
          kind: "err",
          text: "Не удалось сохранить: " + String(e.message || e).slice(0, 100),
        });
      }
    } finally {
      setSavingNudgeDedup(false);
    }
  };

  const performRestart = async () => {
    setRestartConfirm(false);
    setRestarting(true);
    setRestartElapsedSec(0);

    // Локальный таймер прогресса
    const tickStart = Date.now();
    const tickInterval = setInterval(() => {
      setRestartElapsedSec(Math.floor((Date.now() - tickStart) / 1000));
    }, 250);

    try {
      const res = await adminRestart();
      // Сервер примерно через 300мс начнёт os._exit. После этого fetch'и
      // упадут с network error пока новый процесс не поднялся.
      console.log("admin restart response", res);
    } catch (e) {
      // 409 = двойной клик — показать info, не прерывать пользовательский flow
      const msg = String(e.message || e);
      if (msg.includes("409")) {
        clearInterval(tickInterval);
        setRestarting(false);
        if (window.__toast) {
          window.__toast.push({ kind: "info", text: "Перезапуск уже идёт" });
        }
        return;
      }
      // Другая ошибка — но сервер мог всё равно стартануть exit. Продолжаем
      // polling здоровья — если поднимется, всё ок.
      console.warn("admin restart returned error, continuing health polling", e);
    }

    // Polling /api/health пока новый сервер не поднимется
    const POLL_INTERVAL_MS = 500;
    const POLL_TIMEOUT_MS = 30000;  // 30с — с запасом на retry-bind окно (5с)
    const startedAt = Date.now();
    const oldServerStarted = lastHealthStartedAt;
    let detected = false;

    while (Date.now() - startedAt < POLL_TIMEOUT_MS) {
      await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
      try {
        const h = await fetchHealth();
        // Если startedAt > oldServerStarted — это уже новый процесс.
        if (h && h.startedAt && (!oldServerStarted || h.startedAt > oldServerStarted)) {
          detected = true;
          break;
        }
      } catch {
        // network error — ожидаемо во время рестарта.
      }
    }

    clearInterval(tickInterval);
    setRestarting(false);

    if (detected) {
      if (window.__toast) {
        window.__toast.push({ kind: "ok", text: "Веб-сервер успешно перезапущен" });
      }
      // Перезагружаем страницу — забираем свежий JSX (cache-bust по startup_ts).
      setTimeout(() => {
        try { window.location.reload(); } catch {}
      }, 800);
    } else {
      if (window.__toast) {
        window.__toast.push({
          kind: "err",
          text: "Сервер не ответил за 30 сек. Смотри panel_data/logs/restart.log",
        });
      }
    }
  };

  return (
    <SettingsSection
      title="Управление процессами"
      subtitle="Авто-старт бота при подъёме веб-сервера и перезапуск самого веб-сервера без taskkill."
    >
      {/* Тумблер автозапуска */}
      <div className="flex items-start justify-between gap-4 p-4 rounded-xl bg-zinc-900/40 border border-zinc-800/60 mb-4">
        <div className="flex-1 min-w-0">
          <div className="text-sm font-medium text-zinc-100">Автозапуск бота</div>
          <div className="text-[12px] text-zinc-500 mt-1 leading-relaxed">
            Когда включено — бот стартует автоматически каждый раз когда поднимается веб-сервер.
            Это нужно чтобы после «Перезапустить веб-сервер» бот тоже поднимался без ручного клика.
          </div>
        </div>
        <button
          onClick={onToggleAutostart}
          disabled={autostart === null || savingAutostart}
          className={
            "relative w-11 h-6 rounded-full transition-colors shrink-0 " +
            (autostart === null
              ? "bg-zinc-700/50 cursor-wait"
              : autostart
              ? "bg-emerald-500"
              : "bg-zinc-700") +
            (savingAutostart ? " opacity-70" : "")
          }
          aria-label="Автозапуск бота"
        >
          <span
            className={
              "absolute top-0.5 w-5 h-5 rounded-full bg-white transition-transform " +
              (autostart ? "translate-x-5" : "translate-x-0.5")
            }
          />
        </button>
      </div>

      {/* Anti-repetition guard для nudge (Batch 2) */}
      <div className="flex items-start justify-between gap-4 p-4 rounded-xl bg-zinc-900/40 border border-zinc-800/60 mb-4">
        <div className="flex-1 min-w-0">
          <div className="text-sm font-medium text-zinc-100">Anti-repetition для nudge</div>
          <div className="text-[12px] text-zinc-500 mt-1 leading-relaxed">
            Запрещает боту слать одну и ту же фразу подряд при автоматических пингах
            («ну что, ещё думаешь?» × 50 раз). Первый дубль — один re-gen LLM-вызов,
            повторный — skip без вызова. Persistent циклы (24ч/72ч) не блокируются.
            Если конверсия упадёт — выключи переключателем, рестарт не нужен.
          </div>
        </div>
        <button
          onClick={onToggleNudgeDedup}
          disabled={nudgeDedup === null || savingNudgeDedup}
          className={
            "relative w-11 h-6 rounded-full transition-colors shrink-0 " +
            (nudgeDedup === null
              ? "bg-zinc-700/50 cursor-wait"
              : nudgeDedup
              ? "bg-emerald-500"
              : "bg-zinc-700") +
            (savingNudgeDedup ? " opacity-70" : "")
          }
          aria-label="Anti-repetition для nudge"
        >
          <span
            className={
              "absolute top-0.5 w-5 h-5 rounded-full bg-white transition-transform " +
              (nudgeDedup ? "translate-x-5" : "translate-x-0.5")
            }
          />
        </button>
      </div>

      {/* Перезапуск веб-сервера */}
      <div className="p-4 rounded-xl bg-zinc-900/40 border border-zinc-800/60">
        <div className="text-sm font-medium text-zinc-100">Перезапустить веб-сервер</div>
        <div className="text-[12px] text-zinc-500 mt-1 leading-relaxed">
          Полный рестарт uvicorn — нужен после обновления Python-кода.
          Текущая сессия прервётся на ~5 секунд. Бот переживёт рестарт независимо.
          Если автозапуск выше включён — бот тоже поднимется с новым кодом.
        </div>
        <button
          onClick={() => setRestartConfirm(true)}
          disabled={restarting}
          className="mt-3 flex items-center gap-2 px-4 py-2 rounded-lg bg-rose-500/20 border border-rose-500/40 text-rose-200 hover:bg-rose-500/30 disabled:opacity-50 transition-colors text-sm font-medium"
        >
          <Icon name="refresh" className="w-4 h-4" />
          {restarting ? `Перезагружается… ${restartElapsedSec}с` : "Перезапустить веб-сервер"}
        </button>

        {/* lastRestart info */}
        {lastRestart && lastRestart.at && (
          <div className="text-[11px] text-zinc-600 mt-3 font-mono">
            последний рестарт: {String(lastRestart.at).slice(0, 19).replace("T", " ")}
            {lastRestart.newPid ? ` → новый pid=${lastRestart.newPid}` : ""}
          </div>
        )}
      </div>

      {/* Confirmation dialog */}
      {restartConfirm && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
          <div className="max-w-md w-full mx-4 bg-zinc-900 border border-zinc-700 rounded-2xl p-6 shadow-2xl">
            <div className="flex items-center gap-3 mb-3">
              <div className="w-9 h-9 rounded-lg bg-rose-500/20 flex items-center justify-center shrink-0">
                <Icon name="alert" className="w-5 h-5 text-rose-400" />
              </div>
              <div className="font-semibold text-zinc-100">Перезапустить веб-сервер?</div>
            </div>
            <div className="text-sm text-zinc-400 leading-relaxed mb-5">
              Текущая сессия прервётся на ~5 секунд. Бот продолжит работу.
              Если автозапуск включён, бот поднимется автоматически с новым кодом.
            </div>
            <div className="flex items-center justify-end gap-2">
              <button
                onClick={() => setRestartConfirm(false)}
                className="px-4 py-2 rounded-lg text-sm text-zinc-300 hover:bg-zinc-800 transition-colors"
              >
                Отмена
              </button>
              <button
                onClick={performRestart}
                className="px-4 py-2 rounded-lg bg-rose-500 hover:bg-rose-600 text-white text-sm font-medium transition-colors"
              >
                Перезапустить
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Full-screen overlay во время рестарта */}
      {restarting && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-zinc-950/90 backdrop-blur-sm">
          <div className="text-center px-6">
            <div className="w-12 h-12 mx-auto mb-4 border-4 border-zinc-800 border-t-indigo-500 rounded-full animate-spin" />
            <div className="text-zinc-200 font-medium text-lg">Перезагружается веб-сервер…</div>
            <div className="text-zinc-500 text-sm mt-1">прошло {restartElapsedSec}с</div>
            <div className="text-zinc-600 text-xs mt-4 max-w-xs">
              Страница автоматически обновится, как только новый процесс ответит.
              Если не поднимется за 30с — смотри panel_data/logs/restart.log.
            </div>
          </div>
        </div>
      )}
    </SettingsSection>
  );
}

// Cache последнего startedAt из /api/health — используется для детекции
// что после рестарта поднялся НОВЫЙ процесс (а не тот же самый что не успел упасть).
let lastHealthStartedAt = null;

// Auto-toast: при первой загрузке UI проверяем health.autostartBot.
// Если бот был auto-started на этом подъёме сервера — один раз показываем
// toast. localStorage хранит startedAt, чтобы при F5 в той же сессии toast
// не повторялся.
async function _checkAutostartToast() {
  try {
    const h = await fetchHealth();
    if (!h) return;
    lastHealthStartedAt = h.startedAt;
    const a = h.autostartBot;
    if (!a) return;
    const lastShownKey = "zxc.autostart.shownFor";
    const lastShown = window.localStorage.getItem(lastShownKey);
    const currentKey = String(h.startedAt);
    if (lastShown === currentKey) return; // уже показывали для этого подъёма
    if (a.started === true && a.wasRunning === false) {
      if (window.__toast) {
        window.__toast.push({
          kind: "ok",
          text: "Бот автоматически запущен",
        });
      }
    } else if (a.error) {
      if (window.__toast) {
        window.__toast.push({
          kind: "err",
          text: "Автозапуск бота не удался: " + String(a.error).slice(0, 120),
        });
      }
    }
    try { window.localStorage.setItem(lastShownKey, currentKey); } catch {}
  } catch (e) {
    // health недоступен — это могло быть во время рестарта. Молча.
  }
}

// Запускаем проверку один раз при загрузке скрипта.
if (typeof window !== "undefined") {
  // Небольшая задержка — даём React смонтировать toast-контейнер.
  setTimeout(_checkAutostartToast, 800);
}

Object.assign(window, { RequisitesPage, ContentPage, SettingsPage, ProcessesSection });
