/* global React, ReactDOM, CR_UI, CR_QUEUE, CR_ARTICLES, CR_POSTS, CR_SETTINGS, CR_JOBLOG, CR_REPORT, CR_API */
const { useState, useEffect, useCallback, useRef } = React;
const { Icons, Btn, Modal, Toast } = CR_UI;

// Preset rejection reasons offered as quick-pick chips in the reject modal.
// UI-only constant — same list for every brand. Per-brand customisation is
// out of scope for the MVP; if needed later, move to brand.yaml.
const REJECT_REASON_PRESETS = [
  "Не интересно",
  "Уже было",
  "Не наш тон",
  "Слабый источник",
  "Старая новость",
  "Не про Москву",
];

// Convert backend TopicListItem → UI shape used by queue.jsx mocks.
function topicFromApi(t) {
  return {
    id: t.id,
    title: t.title,
    summary: t.summary,
    sources: (t.sources || []).map((u) => {
      try { return new URL(u).hostname.replace(/^www\./, ""); } catch (e) { return u; }
    }),
    keywords: t.keywords || [],
    found_at: t.found_at ? Date.parse(t.found_at) : Date.now(),
    rejected_at: t.rejected_at ? Date.parse(t.rejected_at) : null,
    reason: t.reject_reason,
  };
}

const NAV = [
  { id: "queue",    label: "Очередь",    icon: Icons.Inbox    },
  { id: "articles", label: "Статьи",     icon: Icons.Doc      },
  { id: "posts",    label: "Посты",      icon: Icons.Send     },
  { id: "report",   label: "Отчёт",      icon: Icons.Info     },
  { id: "settings", label: "Настройки",  icon: Icons.Settings },
];

const VALID_SCREENS = new Set(["queue", "articles", "posts", "report", "settings"]);

// Parse #{brand}/{screen} or legacy #{screen}
// Returns { brand: string|null, screen: string }
function parseHash() {
  const raw = (window.location.hash || "").replace(/^#\/?/, "");
  if (!raw) return { brand: null, screen: "queue" };
  const parts = raw.split("/");
  if (parts.length >= 2) {
    const [b, s] = parts;
    return { brand: b || null, screen: VALID_SCREENS.has(s) ? s : "queue" };
  }
  // Legacy #{screen}
  return { brand: null, screen: VALID_SCREENS.has(parts[0]) ? parts[0] : "queue" };
}

const App = () => {
  const [theme, setTheme] = useState(() => localStorage.getItem("cr-theme") || "light");
  const [screen, setScreen] = useState(() => parseHash().screen);
  const [brands, setBrands] = useState([]);
  const [activeBrand, setActiveBrand] = useState(() => parseHash().brand || localStorage.getItem("cr_active_brand") || null);
  const [brandDropOpen, setBrandDropOpen] = useState(false);
  const [newBrandModal, setNewBrandModal] = useState(false);
  const [newBrandSlug, setNewBrandSlug] = useState("");
  const [newBrandChannel, setNewBrandChannel] = useState("");
  const [newBrandCreating, setNewBrandCreating] = useState(false);
  const [queueState, setQueueState] = useState("success");
  const [articlesState, setArticlesState] = useState("success");
  const [postsState, setPostsState] = useState("success");
  const [joblogOpen, setJoblogOpen] = useState(false);
  const [rejectingTopic, setRejectingTopic] = useState(null);
  const [refining, setRefining] = useState(false);
  const [toast, setToast] = useState(null);
  const [rejectReason, setRejectReason] = useState("");
  const [rejectChips, setRejectChips] = useState([]);
  const [refineText, setRefineText] = useState("");
  const [topicCounts, setTopicCounts] = useState({ pending: 0, approved: 0, rejected: 0 });
  const [topics, setTopics] = useState({ pending: [], approved: [], rejected: [] });
  const [apiOnline, setApiOnline] = useState(null);
  const [postCounts, setPostCounts] = useState({ drafts: 0, published: 0 });
  const [footerStats, setFooterStats] = useState(null); // { tokens_this_month, last_bootstrap_at } or null
  const [footerYaml, setFooterYaml] = useState(null); // brand.yaml for auto_discover_at
  const [searchOpen, setSearchOpen] = useState(false);
  const [searchQuery, setSearchQuery] = useState("");
  const [searchResults, setSearchResults] = useState(null); // null = idle, {topics,research,posts,total} = ready
  const [searchLoading, setSearchLoading] = useState(false);
  const [activeJobs, setActiveJobs] = useState([]);
  const [pendingNavTarget, setPendingNavTarget] = useState(null); // {kind, id, folder}
  const dropRef = useRef(null);
  const searchInputRef = useRef(null);

  useEffect(() => {
    document.documentElement.setAttribute("data-theme", theme);
    localStorage.setItem("cr-theme", theme);
  }, [theme]);

  // Load brand list on startup; set activeBrand if not already set or stale
  useEffect(() => {
    CR_API.listBrands().then((list) => {
      setBrands(list);
      setActiveBrand((cur) => {
        if (cur && list.includes(cur)) return cur;
        const first = list[0] || null;
        if (first) localStorage.setItem("cr_active_brand", first);
        return first;
      });
    }).catch(() => {});
  }, []);

  // Persist activeBrand to localStorage and update URL hash
  useEffect(() => {
    if (!activeBrand) return;
    localStorage.setItem("cr_active_brand", activeBrand);
    const expected = `#${activeBrand}/${screen}`;
    if (window.location.hash !== expected) {
      window.history.replaceState(null, "", expected);
    }
  }, [activeBrand, screen]);

  // Hash ↔ screen sync — survives F5 and Back/Forward
  useEffect(() => {
    const onHashChange = () => {
      const { brand: b, screen: s } = parseHash();
      setScreen((cur) => cur === s ? cur : s);
      if (b) setActiveBrand((cur) => cur === b ? cur : b);
    };
    window.addEventListener("hashchange", onHashChange);
    window.addEventListener("popstate", onHashChange);
    return () => {
      window.removeEventListener("hashchange", onHashChange);
      window.removeEventListener("popstate", onHashChange);
    };
  }, []);

  const goToScreen = useCallback((next) => {
    if (!VALID_SCREENS.has(next)) return;
    setActiveBrand((brand) => {
      const h = brand ? `#${brand}/${next}` : `#${next}`;
      if (window.location.hash !== h) window.history.pushState(null, "", h);
      return brand;
    });
    setScreen(next);
  }, []);

  // Close brand dropdown on outside click
  useEffect(() => {
    if (!brandDropOpen) return;
    const handler = (e) => { if (dropRef.current && !dropRef.current.contains(e.target)) setBrandDropOpen(false); };
    document.addEventListener("mousedown", handler);
    return () => document.removeEventListener("mousedown", handler);
  }, [brandDropOpen]);

  useEffect(() => {
    if (toast) {
      const t = setTimeout(() => setToast(null), 2400);
      return () => clearTimeout(t);
    }
  }, [toast]);

  const showToast = (msg, hash) => setToast({ msg, hash });

  // Fetch topics with AbortController so stale brand's requests can't overwrite
  const refreshTopics = useCallback(async (brand, signal) => {
    if (!brand) return;
    try {
      const [pending, approved, rejected] = await Promise.all([
        CR_API.listTopics(brand, "pending"),
        CR_API.listTopics(brand, "approved"),
        CR_API.listTopics(brand, "rejected"),
      ]);
      if (signal?.aborted) return;
      const mapped = {
        pending: pending.map(topicFromApi),
        approved: approved.map(topicFromApi),
        rejected: rejected.map(topicFromApi),
      };
      setTopics(mapped);
      setTopicCounts({ pending: mapped.pending.length, approved: mapped.approved.length, rejected: mapped.rejected.length });
      setApiOnline(true);
    } catch (e) {
      if (signal?.aborted) return;
      console.warn("API offline:", e.message);
      setApiOnline(false);
    }
  }, []);

  // Re-fetch topics when activeBrand changes; abort previous in-flight requests
  useEffect(() => {
    if (!activeBrand) return;
    const ctrl = new AbortController();
    refreshTopics(activeBrand, ctrl.signal);
    return () => ctrl.abort();
  }, [activeBrand, refreshTopics]);

  // Refresh post counts so sidebar matches PostsScreen lists
  const refreshPostCounts = useCallback(async (brand) => {
    if (!brand) return;
    try {
      const [d, p] = await Promise.all([
        CR_API.listPostDrafts(brand).catch(() => []),
        CR_API.listPostsPublished(brand).catch(() => []),
      ]);
      setPostCounts({ drafts: d.length, published: p.length });
    } catch (_) {}
  }, []);

  useEffect(() => {
    if (!activeBrand) return;
    refreshPostCounts(activeBrand);
    const t = setInterval(() => refreshPostCounts(activeBrand), 8000);
    return () => clearInterval(t);
  }, [activeBrand, refreshPostCounts]);

  // Footer stats (tokens this month + auto-discover schedule + last bootstrap)
  useEffect(() => {
    if (!activeBrand) { setFooterStats(null); setFooterYaml(null); return; }
    let cancelled = false;
    Promise.all([
      CR_API.getBrandStats(activeBrand).catch(() => null),
      CR_API.getBrandYaml(activeBrand).catch(() => null),
    ]).then(([stats, yaml]) => {
      if (cancelled) return;
      setFooterStats(stats);
      setFooterYaml(yaml);
    });
    return () => { cancelled = true; };
  }, [activeBrand]);

  // Poll active jobs for the current brand — used for honest queue/article banners
  useEffect(() => {
    if (!activeBrand) { setActiveJobs([]); return; }
    let cancelled = false;
    async function load() {
      try {
        const list = await CR_API.listJobs(activeBrand, true);
        if (!cancelled) setActiveJobs(list);
      } catch (_) {}
    }
    load();
    const t = setInterval(load, 4000);
    return () => { cancelled = true; clearInterval(t); };
  }, [activeBrand]);

  // ⌘K / Ctrl+K opens search; Esc closes; "/" focuses when not in input
  useEffect(() => {
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
        e.preventDefault();
        setSearchOpen(true);
        setTimeout(() => searchInputRef.current?.focus(), 0);
      } else if (e.key === "Escape" && searchOpen) {
        setSearchOpen(false);
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [searchOpen]);

  // Debounced backend search
  useEffect(() => {
    if (!searchOpen) return;
    if (!activeBrand) return;
    const q = searchQuery.trim();
    if (!q) { setSearchResults(null); return; }
    setSearchLoading(true);
    const t = setTimeout(async () => {
      try {
        const r = await CR_API.search(activeBrand, q);
        setSearchResults(r);
      } catch (e) {
        setSearchResults({ query: q, topics: [], research: [], posts: [], total: 0, error: e.message });
      } finally {
        setSearchLoading(false);
      }
    }, 200);
    return () => clearTimeout(t);
  }, [searchQuery, searchOpen, activeBrand]);

  const handleApprove = async (t) => {
    try {
      await CR_API.approveTopic(activeBrand, t.id);
      // Try to auto-launch deep_research; the brand-level job picks up all
      // approved topics that don't have research yet. Skip silently if a
      // job is already running — it will pick up this topic on its sweep.
      let researchStarted = false;
      try {
        await CR_API.triggerDeepResearch(activeBrand);
        researchStarted = true;
      } catch (err) {
        if (err.status !== 409) throw err;
      }
      showToast(
        `«${t.title.slice(0, 40)}…» одобрено${researchStarted ? " · ресерч запущен" : " · ресерч уже идёт"}`,
        t.id,
      );
      refreshTopics(activeBrand);
    } catch (e) {
      showToast("Ошибка: " + e.message);
    }
  };
  const handleReject = (t) => { setRejectingTopic(t); setRejectReason(""); setRejectChips([]); };
  const submitReject = async () => {
    const t = rejectingTopic;
    const reason = rejectReason.trim() || rejectChips.join(", ") || "без причины";
    try {
      await CR_API.rejectTopic(activeBrand, t.id, reason);
      showToast(`«${t.title.slice(0, 40)}…» отклонено`);
      refreshTopics(activeBrand);
    } catch (e) {
      showToast("Ошибка: " + e.message);
    }
    setRejectingTopic(null);
  };
  const handleRestore = async (t) => {
    try {
      await CR_API.restoreTopic(activeBrand, t.id);
      showToast(`«${t.title.slice(0, 40)}…» возвращено в очередь`);
      refreshTopics(activeBrand);
    } catch (e) {
      showToast("Ошибка: " + e.message);
    }
  };
  const handleOpenArticle = (topic) => {
    if (topic && topic.id) setPendingNavTarget({ kind: "topic", id: topic.id });
    goToScreen("articles");
  };
  const handleFindNew = async () => {
    if (!activeBrand) return;
    setQueueState("loading");
    setJoblogOpen(true);
    try {
      const job = await CR_API.triggerFindTopics(activeBrand);
      showToast("Поиск тем запущен · job " + job.id, job.id);
      // Don't claim success — job runs in background; queue state stays "loading"
      // until polling sees the topic list change. Reset to neutral after 2s.
      setTimeout(() => setQueueState("success"), 2000);
    } catch (e) {
      if (e.status === 409) {
        showToast("Уже идёт другой job для этого бренда");
      } else {
        showToast("Ошибка: " + e.message);
      }
      setQueueState("error");
    }
  };
  const handleRunResearchForTopic = async (topic) => {
    if (!activeBrand || !topic) return;
    try {
      const job = await CR_API.triggerDeepResearch(activeBrand);
      showToast("Запущен deep_research · job " + job.id, job.id);
      setJoblogOpen(true);
    } catch (e) {
      if (e.status === 409) showToast("Уже идёт другой job для этого бренда");
      else showToast("Ошибка: " + e.message);
    }
  };
  const handleGeneratePost = async (topic) => {
    if (!activeBrand || !topic?.id) return;
    try {
      const job = await CR_API.generatePost(activeBrand, topic.id);
      showToast("Генерация поста запущена · job " + job.job_id, job.job_id);
      setJoblogOpen(true);
      goToScreen("posts");
    } catch (e) {
      if (e.status === 400) showToast("Сначала запустите ресерч — версии нет");
      else if (e.status === 409) showToast("Уже идёт другой job для этого бренда");
      else showToast("Ошибка: " + e.message);
    }
  };
  const handleSubmitRefine = async (topic, comment) => {
    if (!activeBrand || !topic?.id || !comment) { setRefining(false); return; }
    try {
      await CR_API.refineResearch(activeBrand, topic.id, comment);
      showToast("Уточнение запущено — Claude напишет новую версию", topic.id);
      setJoblogOpen(true);
    } catch (e) {
      if (e.status === 404) showToast("Нет ресерча — сначала запустите deep_research");
      else if (e.status === 409) showToast("Уже идёт другой job для этого бренда");
      else showToast("Ошибка: " + e.message);
    }
    setRefining(false);
  };
  const handleBootstrap = async (channel, limit = 100) => {
    if (!channel) { showToast("Укажи канал в TG"); return; }
    try {
      await CR_API.telegramFetchHistory(activeBrand, channel, limit);
      showToast(`Читаю ${limit} постов канала ${channel}…`);
      let lastPhase = null;
      const interval = setInterval(async () => {
        try {
          const s = await CR_API.telegramStatus(activeBrand);
          if (s.phase && s.phase !== lastPhase) {
            lastPhase = s.phase;
            if (s.phase === "analyzing") {
              setJoblogOpen(true);
              showToast(`Скачано ${s.downloaded} постов · Claude анализирует…`, s.claude_job_id || "");
            } else if (s.phase === "downloaded") {
              showToast(`Скачано ${s.downloaded} постов`, "downloaded");
            }
          }
          if (s.status === "ok") {
            clearInterval(interval);
            if (s.phase === "analyzed") {
              showToast(`Готово: ${s.downloaded} постов → settings.md и style_guide.md обновлены`, "bootstrap-ok");
            } else {
              showToast(`Скачано ${s.downloaded} постов из ${channel}`, "ok");
            }
          } else if (s.status === "failed") {
            clearInterval(interval);
            const where = s.phase === "analyzing" ? "Claude" : "загрузка";
            showToast(`Ошибка (${where}): ${s.error || "unknown"}`);
          }
        } catch (e) { clearInterval(interval); showToast("Ошибка статуса: " + e.message); }
      }, 1500);
    } catch (e) {
      if (e.status === 409) showToast("Уже идёт фоновая загрузка для этого бренда");
      else showToast("Ошибка: " + e.message);
    }
  };

  const handleSave = async (file) => {
    const map = { "settings.md": "settings", "style_guide.md": "style", "sources.md": "sources" };
    const key = map[file];
    if (!key) { showToast(`${file} сохранён · git`, "a3f9c12"); return; }
    showToast(`${file} сохранён · git`, "локально");
  };
  const handleCopy = (post) => {
    if (post && post.text && navigator.clipboard) {
      navigator.clipboard.writeText(post.text).catch(() => {});
    }
    showToast("Скопировано в буфер обмена");
  };
  const handlePostsChanged = () => {
    refreshPostCounts(activeBrand);
  };

  // Active job for the queue banner
  const findTopicsJob = activeJobs.find((j) => j.kind === "find_topics" && (j.status === "running" || j.status === "queued"));
  const queueLoading = !!findTopicsJob || queueState === "loading";
  const computedQueueState = queueLoading ? "loading" : queueState;

  // Auto-refresh topics when find_topics job transitions running → done.
  // Job poller (4s) updates activeJobs; we detect the disappearance of a
  // previously-running find_topics and refetch pending/approved/rejected.
  // AbortController guards against brand-switch race: if activeBrand changes
  // before the fetch resolves, refreshTopics drops the stale response.
  const prevFindTopicsRunningRef = useRef(false);
  useEffect(() => {
    const isRunning = !!findTopicsJob;
    const justFinished = prevFindTopicsRunningRef.current && !isRunning;
    prevFindTopicsRunningRef.current = isRunning;
    if (!justFinished || !activeBrand) return;
    const ctrl = new AbortController();
    refreshTopics(activeBrand, ctrl.signal);
    return () => ctrl.abort();
  }, [findTopicsJob, activeBrand, refreshTopics]);

  return (
    <div className="app">
      {/* TOPBAR */}
      <header className="topbar">
        <div className="topbar__brand">
          <div className="topbar__logo" />
          <div className="topbar__name">Content Radar</div>
        </div>
        <div className="topbar__divider" />

        {/* Brand selector */}
        <div ref={dropRef} style={{ position: "relative" }}>
          <button
            className="topbar__brand-select"
            title="Сменить бренд"
            onClick={() => setBrandDropOpen((o) => !o)}
          >
            <span className="topbar__brand-dot" />
            <span style={{ fontWeight: 500 }}>{activeBrand || "—"}</span>
            <span style={{ color: "var(--fg-muted)", fontSize: 11, fontFamily: "TT Lakes", letterSpacing: "0.1em", textTransform: "uppercase" }}>Бренд</span>
            <Icons.ChevronDown size={14} style={{ color: "var(--fg-muted)" }} />
          </button>
          {brandDropOpen && (
            <ul style={{
              position: "absolute", top: "calc(100% + 4px)", left: 0, minWidth: 200,
              background: "var(--bg-page, #fff)", border: "1px solid var(--border)", borderRadius: 8,
              boxShadow: "0 8px 24px rgba(0,0,0,.18)", padding: "4px 0", zIndex: 9999, listStyle: "none", margin: 0,
            }}>
              {brands.length === 0 && (
                <li style={{ padding: "8px 14px", color: "var(--fg-muted)", fontSize: 13 }}>Нет брендов</li>
              )}
              {brands.map((b) => (
                <li key={b} style={{ display: "flex", alignItems: "center" }}
                  onMouseEnter={(e) => e.currentTarget.style.background = "var(--surface-hover, #f5f5f5)"}
                  onMouseLeave={(e) => e.currentTarget.style.background = ""}>
                  <span style={{ flex: 1, padding: "8px 14px", fontSize: 13, cursor: "pointer",
                    fontWeight: b === activeBrand ? 600 : 400,
                    color: b === activeBrand ? "var(--accent)" : "var(--fg)" }}
                    onClick={() => { setActiveBrand(b); localStorage.setItem("cr_active_brand", b); setBrandDropOpen(false); }}>
                    {b === activeBrand && "✓ "}{b}
                  </span>
                  <span title="Удалить бренд" style={{ padding: "8px 10px", cursor: "pointer", color: "var(--fg-muted)", fontSize: 12 }}
                    onMouseEnter={(e) => { e.currentTarget.style.color = "var(--status-error-fg)"; }}
                    onMouseLeave={(e) => { e.currentTarget.style.color = "var(--fg-muted)"; }}
                    onClick={async (e) => {
                      e.stopPropagation();
                      if (!confirm(`Удалить бренд «${b}» и все его данные? Это необратимо.`)) return;
                      try {
                        await CR_API.deleteBrand(b);
                        const list = await CR_API.listBrands();
                        setBrands(list);
                        if (activeBrand === b) {
                          const next = list[0] || null;
                          setActiveBrand(next);
                          if (next) localStorage.setItem("cr_active_brand", next);
                          else localStorage.removeItem("cr_active_brand");
                        }
                        setBrandDropOpen(false);
                        showToast(`Бренд «${b}» удалён`);
                      } catch (err) { showToast("Ошибка: " + err.message); }
                    }}>✕</span>
                </li>
              ))}
              <li style={{ borderTop: "1px solid var(--border)", margin: "4px 0" }} />
              <li onClick={() => { setBrandDropOpen(false); setNewBrandSlug(""); setNewBrandChannel(""); setNewBrandModal(true); }}
                style={{ padding: "8px 14px", fontSize: 13, cursor: "pointer", color: "var(--accent)" }}
                onMouseEnter={(e) => e.currentTarget.style.background = "var(--surface-hover, #f5f5f5)"}
                onMouseLeave={(e) => e.currentTarget.style.background = ""}>
                + Новый бренд
              </li>
            </ul>
          )}
        </div>

        <div className="topbar__spacer" />
        <div className="topbar__theme" role="tablist" aria-label="Тема">
          <button className={theme === "light" ? "is-on" : ""} onClick={() => setTheme("light")} title="Дневная тема (L)">
            <Icons.Sun size={13} /> Светлая
          </button>
          <button className={theme === "dark" ? "is-on" : ""} onClick={() => setTheme("dark")} title="Ночная тема (D)">
            <Icons.Moon size={13} /> Тёмная
          </button>
        </div>
        <button
          className="topbar__icon-btn"
          title="Поиск (⌘K)"
          onClick={() => { setSearchOpen(true); setTimeout(() => searchInputRef.current?.focus(), 0); }}
        ><Icons.Search size={14} /></button>
        <div className="topbar__user" title="Один пользователь — оператор">ОП</div>
      </header>

      {/* SIDEBAR */}
      <aside className="sidebar">
        <div className="sidebar__section-label">Рабочее пространство</div>
        {NAV.map((n) => {
          const Iconc = n.icon;
          let count;
          if (n.id === "queue") count = topicCounts.pending;
          else if (n.id === "articles") count = topicCounts.approved;
          else if (n.id === "posts") count = postCounts.drafts + postCounts.published;
          return (
            <div key={n.id}
              className={"nav-item " + (screen === n.id ? "is-active" : "")}
              onClick={() => goToScreen(n.id)}>
              <Iconc size={16} style={{ color: "currentColor" }} className="nav-item__icon" />
              <span>{n.label}</span>
              {count != null && <span className="nav-item__count">{count}</span>}
            </div>
          );
        })}
        <div style={{ flex: 1 }} />
        <div className="sidebar__footer">
          <div className="sidebar__footer-row">
            <span>API</span>
            <span className={apiOnline === false ? "" : "accent"}>
              ● {apiOnline === false ? "недоступен" : "подключён"}
            </span>
          </div>
          <div className="sidebar__footer-row">
            <span>Автозапуск</span>
            <span>{footerYaml?.auto_discover_at || "—"}</span>
          </div>
          <div className="sidebar__footer-row">
            <span>Токены · {new Date().toLocaleString("ru-RU", { month: "long" })}</span>
            <span>
              {footerStats?.tokens_this_month != null
                ? footerStats.tokens_this_month.toLocaleString("ru-RU")
                : "—"}
            </span>
          </div>
        </div>
      </aside>

      {/* MAIN */}
      <main className="main" data-screen-label={screen}>
        {screen === "queue" && (
          <CR_QUEUE.QueueScreen
            brand={activeBrand}
            state={computedQueueState}
            runningJobKind={findTopicsJob?.kind}
            runningJobElapsed={findTopicsJob ? Math.round((Date.now() - Date.parse(findTopicsJob.started_at)) / 1000) : null}
            onOpenJobLog={() => setJoblogOpen(true)}
            onApprove={handleApprove}
            onReject={handleReject}
            onRestore={handleRestore}
            onOpenArticle={handleOpenArticle}
            onFindNew={handleFindNew}
            onRefresh={() => refreshTopics(activeBrand)}
            topicsOverride={topics}
          />
        )}
        {screen === "articles" && (
          <CR_ARTICLES.ArticlesScreen
            brand={activeBrand}
            navTarget={pendingNavTarget?.kind === "topic" ? pendingNavTarget : null}
            onNavConsumed={() => setPendingNavTarget(null)}
            activeJobs={activeJobs}
            onGeneratePost={handleGeneratePost}
            onRunResearch={handleRunResearchForTopic}
            onRefine={(topic) => { setRefining(topic || true); setRefineText(""); }}
            onOpenJobLog={() => setJoblogOpen(true)}
          />
        )}
        {screen === "posts" && (
          <CR_POSTS.PostsScreen
            brand={activeBrand}
            navTarget={pendingNavTarget}
            onNavConsumed={() => setPendingNavTarget(null)}
            onCopy={handleCopy}
            onPostsChanged={handlePostsChanged}
          />
        )}
        {screen === "report" && (
          <CR_REPORT.ReportScreen brand={activeBrand} />
        )}
        {screen === "settings" && (
          <CR_SETTINGS.SettingsScreen
            brand={activeBrand}
            onSave={handleSave}
            onBootstrap={handleBootstrap}
          />
        )}
      </main>

      {/* JOBLOG */}
      <div className="joblog-host">
        <CR_JOBLOG.JobLog
          open={joblogOpen}
          onToggle={() => setJoblogOpen(!joblogOpen)}
          brand={activeBrand}
          anyActive={true}
        />
      </div>

      {/* REJECT MODAL */}
      {rejectingTopic && (
        <Modal
          title="Отклонить тему"
          onClose={() => setRejectingTopic(null)}
          footer={
            <>
              <Btn kind="ghost" onClick={() => setRejectingTopic(null)}>Отмена</Btn>
              <Btn kind="primary" icon={<Icons.X size={14} />} onClick={submitReject}>Отклонить</Btn>
            </>
          }
        >
          <div style={{ color: "var(--fg-muted)", fontSize: 13, marginBottom: 12 }}>
            «{rejectingTopic.title}»
          </div>
          <label className="label">Причина (свободный ввод)</label>
          <textarea
            className="textarea"
            placeholder="Например: уже было похожее на прошлой неделе"
            value={rejectReason}
            onChange={(e) => setRejectReason(e.target.value)}
            autoFocus
          />
          <label className="label" style={{ marginTop: 14 }}>Быстрые причины</label>
          <div className="modal__chips">
            {REJECT_REASON_PRESETS.map((r) => (
              <button key={r}
                className={"chip " + (rejectChips.includes(r) ? "is-on" : "")}
                onClick={() => {
                  setRejectChips(rejectChips.includes(r) ? rejectChips.filter(x => x !== r) : [...rejectChips, r]);
                  if (!rejectReason) setRejectReason(r);
                }}>
                {r}
              </button>
            ))}
          </div>
        </Modal>
      )}

      {/* REFINE MODAL */}
      {refining && (
        <Modal
          title="Уточнить статью"
          onClose={() => setRefining(false)}
          footer={
            <>
              <Btn kind="ghost" onClick={() => setRefining(false)}>Отмена</Btn>
              <Btn
                kind="primary"
                icon={<Icons.Sparkles size={14} />}
                disabled={!refineText.trim() || refining === true}
                onClick={() => handleSubmitRefine(refining === true ? null : refining, refineText.trim())}
              >Запустить</Btn>
            </>
          }
        >
          <label className="label">Что нужно уточнить или проверить?</label>
          <textarea
            className="textarea"
            placeholder="Например: проверь источник на цифру 50%, найди свежее исследование"
            value={refineText}
            onChange={(e) => setRefineText(e.target.value)}
            autoFocus
            rows={5}
          />
          <div className="modal__hint">
            <Icons.Info size={14} style={{ flex: "none", marginTop: 1 }} />
            <div>
              Это создаст версию <strong>v4</strong>. История сохраняется — можно вернуться к v1–v3 в любой момент через переключатель сверху.
            </div>
          </div>
        </Modal>
      )}

      {/* NEW BRAND MODAL */}
      {newBrandModal && (
        <Modal
          title="Новый бренд"
          onClose={() => setNewBrandModal(false)}
          footer={
            <>
              <Btn kind="ghost" onClick={() => setNewBrandModal(false)}>Отмена</Btn>
              <Btn kind="primary" disabled={!newBrandSlug.trim() || newBrandCreating}
                onClick={async () => {
                  setNewBrandCreating(true);
                  try {
                    await CR_API.createBrand({ slug: newBrandSlug.trim(), channel: newBrandChannel.trim() || null });
                    const list = await CR_API.listBrands();
                    setBrands(list);
                    setActiveBrand(newBrandSlug.trim());
                    localStorage.setItem("cr_active_brand", newBrandSlug.trim());
                    setNewBrandModal(false);
                    showToast(`Бренд «${newBrandSlug.trim()}» создан`);
                  } catch (e) {
                    showToast("Ошибка: " + e.message);
                  } finally {
                    setNewBrandCreating(false);
                  }
                }}>
                {newBrandCreating ? "Создаём…" : "Создать"}
              </Btn>
            </>
          }
        >
          <label className="label">Slug бренда <span style={{ color: "var(--status-error-fg)" }}>*</span></label>
          <input className="input" value={newBrandSlug} onChange={(e) => setNewBrandSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""))}
            placeholder="my-brand" autoFocus />
          <div style={{ fontSize: 11, color: "var(--fg-muted)", marginTop: 4 }}>Только строчные латинские буквы, цифры, дефисы</div>
          <label className="label" style={{ marginTop: 14 }}>Telegram-канал</label>
          <input className="input" value={newBrandChannel} onChange={(e) => setNewBrandChannel(e.target.value)}
            placeholder="@username" />
        </Modal>
      )}

      {/* SEARCH PALETTE */}
      {searchOpen && (
        <div className="search-overlay" onClick={() => setSearchOpen(false)}>
          <div className="search-palette" onClick={(e) => e.stopPropagation()}>
            <div className="search-palette__input-row">
              <Icons.Search size={16} style={{ color: "var(--fg-muted)", flex: "none" }} />
              <input
                ref={searchInputRef}
                className="search-palette__input"
                value={searchQuery}
                onChange={(e) => setSearchQuery(e.target.value)}
                placeholder={activeBrand ? `Поиск по «${activeBrand}»…` : "Сначала выберите бренд"}
                disabled={!activeBrand}
                autoFocus
              />
              <span className="kbd">Esc</span>
            </div>
            <div className="search-palette__body">
              {searchLoading && <div className="search-palette__hint">Ищу…</div>}
              {!searchLoading && !searchQuery.trim() && (
                <div className="search-palette__hint">Начните вводить — поиск идёт по темам, ресерчу и постам выбранного бренда.</div>
              )}
              {!searchLoading && searchResults && searchResults.total === 0 && (
                <div className="search-palette__hint">Ничего не найдено.</div>
              )}
              {searchResults && searchResults.topics.length > 0 && (
                <div className="search-palette__group">
                  <div className="search-palette__group-label">Темы · {searchResults.topics.length}</div>
                  {searchResults.topics.map((t) => (
                    <button key={"t-" + t.id}
                      className="search-palette__row"
                      onClick={() => {
                        setSearchOpen(false);
                        if (t.status === "approved" || t.status === "done") {
                          setPendingNavTarget({ kind: "topic", id: t.id });
                          goToScreen("articles");
                        } else {
                          goToScreen("queue");
                        }
                      }}>
                      <div className="search-palette__row-title">{t.title}</div>
                      <div className="search-palette__row-meta">{t.status} · {t.id}</div>
                      <div className="search-palette__row-snippet">{t.snippet}</div>
                    </button>
                  ))}
                </div>
              )}
              {searchResults && searchResults.research.length > 0 && (
                <div className="search-palette__group">
                  <div className="search-palette__group-label">Ресерч · {searchResults.research.length}</div>
                  {searchResults.research.map((r) => (
                    <button key={"r-" + r.topic_id + "-" + r.version}
                      className="search-palette__row"
                      onClick={() => {
                        setSearchOpen(false);
                        setPendingNavTarget({ kind: "topic", id: r.topic_id });
                        goToScreen("articles");
                      }}>
                      <div className="search-palette__row-title">{r.topic_id}</div>
                      <div className="search-palette__row-meta">v{r.version}</div>
                      <div className="search-palette__row-snippet">{r.snippet}</div>
                    </button>
                  ))}
                </div>
              )}
              {searchResults && searchResults.posts.length > 0 && (
                <div className="search-palette__group">
                  <div className="search-palette__group-label">Посты · {searchResults.posts.length}</div>
                  {searchResults.posts.map((p) => (
                    <button key={"p-" + p.folder + "-" + p.topic_id}
                      className="search-palette__row"
                      onClick={() => {
                        setSearchOpen(false);
                        setPendingNavTarget({ kind: "post", id: p.topic_id, folder: p.folder });
                        goToScreen("posts");
                      }}>
                      <div className="search-palette__row-title">{p.topic_id}</div>
                      <div className="search-palette__row-meta">{p.folder === "drafts" ? "черновик" : "опубликован"}</div>
                      <div className="search-palette__row-snippet">{p.snippet}</div>
                    </button>
                  ))}
                </div>
              )}
            </div>
          </div>
        </div>
      )}

      {/* TOAST */}
      {toast && <Toast message={toast.msg} hash={toast.hash} />}
    </div>
  );
};

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
