/* ============================================================
   reader.jsx — встроенная читалка: PDF (pdf.js) / EPUB и FB2
   (epub.js — FB2 конвертируется в EPUB на сервере при загрузке) /
   DOC, DOCX (сконвертированы в HTML на сервере через mammoth /
   word-extractor). Прогресс чтения и отметки (highlight) хранятся
   в самом материале (m.progress, m.annotations) и синхронизируются
   через общий механизм PlannerStore → POST /api/state, как и весь
   остальной стейт приложения — отдельного API для этого не нужно.
   Структура интерфейса — по образцу Sumatra PDF: тулбар с полем
   страницы, стрелками, зумом и поиском + боковая панель «Закладки»
   с оглавлением документа.
   ============================================================ */
const React = window.React;
const { useState, useEffect, useRef } = React;
const Icon = window.Icon;
const A = window.PlannerActions;

const HL_COLORS = ["#fff59d", "#a5d6a7", "#90caf9", "#f48fb1"];

/* ---- floating color-picker shown after a text selection -------- */
function SelectionToolbar({ x, y, onPick, onClose }) {
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);
  return (
    <div className="reader-seltoolbar" style={{ left: x, top: y }} onMouseDown={(e) => e.preventDefault()}>
      {HL_COLORS.map((c) => (
        <button key={c} className="reader-seltoolbar__swatch" style={{ background: c }} onClick={() => onPick(c)} title="Выделить" />
      ))}
      <button className="reader-seltoolbar__x" onClick={onClose} title="Отмена"><Icon name="close" size={11} /></button>
    </div>
  );
}

/* ---- боковая панель «Закладки» (оглавление документа) ----------- */
function ReaderSidebar({ items, onGo, onClose }) {
  return (
    <div className="reader-sidebar">
      <div className="reader-sidebar__head">
        <span className="e-label">Закладки</span>
        <button className="reader-sidebar__x" onClick={onClose} title="Скрыть панель"><Icon name="close" size={12} /></button>
      </div>
      <div className="reader-sidebar__list">
        {items.length === 0 && <div className="reader-sidebar__empty">У документа нет оглавления</div>}
        {items.map((it, i) => (
          <button key={i} className="reader-sidebar__item" style={{ paddingLeft: 10 + it.depth * 14 }} onClick={() => onGo(it)}>
            {it.title}
          </button>
        ))}
      </div>
    </div>
  );
}

/* ---- EPUB / FB2-as-EPUB reader (epub.js) ------------------------ */
const EPUB_FONT_SIZES = [90, 100, 115, 130];

function EpubReader({ m }) {
  const containerRef = useRef(null);
  const renditionRef = useRef(null);
  const bookRef = useRef(null);
  const saveTimer = useRef(null);
  const [error, setError] = useState(null);
  const [selection, setSelection] = useState(null);
  const [loading, setLoading] = useState(true);
  const [toc, setToc] = useState([]);
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [fontIdx, setFontIdx] = useState(1);
  const [query, setQuery] = useState("");
  const [results, setResults] = useState(null); /* [{cfi, excerpt}] */
  const [resIdx, setResIdx] = useState(0);
  const [searching, setSearching] = useState(false);

  useEffect(() => {
    let destroyed = false;
    (async () => {
      try {
        const res = await window.authFetch("/api/materials/" + m.id + "/file");
        if (!res.ok) throw new Error("Не удалось загрузить файл книги");
        const buf = await res.arrayBuffer();
        if (destroyed) return;
        const book = window.ePub(buf);
        bookRef.current = book;
        const rendition = book.renderTo(containerRef.current, { width: "100%", height: "100%", flow: "paginated", spread: "none" });
        renditionRef.current = rendition;
        await book.ready;
        if (destroyed) return;

        try {
          const nav = await book.loaded.navigation;
          const flat = (items, depth = 0) => (items || []).flatMap((it) => [
            { title: (it.label || "").trim() || "—", href: it.href, depth },
            ...flat(it.subitems, depth + 1),
          ]);
          setToc(flat(nav.toc));
        } catch (e) {}

        await book.locations.generate(1200);
        if (destroyed) return;
        const startCfi = m.progress.current > 0 ? book.locations.cfiFromPercentage(m.progress.current / 100) : undefined;
        await rendition.display(startCfi || undefined);
        if (destroyed) return;

        (m.annotations || []).forEach((a) => {
          if (a.kind === "epub" && a.cfi) {
            try { rendition.annotations.add("highlight", a.cfi, {}, undefined, "reader-highlight", { fill: a.color, "fill-opacity": "0.35" }); } catch (e) {}
          }
        });

        rendition.on("relocated", (loc) => {
          try {
            const frac = book.locations.percentageFromCfi(loc.start.cfi) || 0;
            const pct = Math.max(0, Math.min(100, Math.round(frac * 100)));
            clearTimeout(saveTimer.current);
            saveTimer.current = setTimeout(() => A.setReaderProgress(m.id, pct), 700);
          } catch (e) {}
        });

        rendition.on("selected", (cfiRange, contents) => {
          try {
            const range = contents.range(cfiRange);
            const rect = range.getBoundingClientRect();
            const iframeRect = containerRef.current.querySelector("iframe").getBoundingClientRect();
            const text = contents.window.getSelection().toString();
            if (!text.trim()) return;
            setSelection({ cfiRange, text, x: iframeRect.left + rect.left + rect.width / 2, y: iframeRect.top + rect.top });
          } catch (e) {}
        });

        setLoading(false);
      } catch (e) {
        if (!destroyed) { setError(e.message); setLoading(false); }
      }
    })();
    return () => {
      destroyed = true;
      clearTimeout(saveTimer.current);
      if (renditionRef.current) renditionRef.current.destroy();
    };
    // eslint-disable-next-line
  }, [m.id, m.file]);

  /* размер шрифта (Aa) */
  useEffect(() => {
    if (renditionRef.current) {
      try { renditionRef.current.themes.fontSize(EPUB_FONT_SIZES[fontIdx] + "%"); } catch (e) {}
    }
  }, [fontIdx, loading]);

  /* переоткрытая панель меняет ширину контейнера — пересчитать размер */
  useEffect(() => {
    const el = containerRef.current;
    if (!el || !renditionRef.current) return;
    requestAnimationFrame(() => {
      try { renditionRef.current.resize(el.clientWidth, el.clientHeight); } catch (e) {}
    });
  }, [sidebarOpen]);

  const goPrev = () => renditionRef.current && renditionRef.current.prev();
  const goNext = () => renditionRef.current && renditionRef.current.next();

  useEffect(() => {
    const onKey = (e) => {
      if (e.target && (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")) return;
      if (e.key === "ArrowLeft") goPrev();
      if (e.key === "ArrowRight") goNext();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  const runSearch = async (e) => {
    if (e) e.preventDefault();
    const q = query.trim();
    const book = bookRef.current;
    if (!q || !book) { setResults(null); return; }
    setSearching(true);
    try {
      const found = [];
      const spineItems = book.spine.spineItems || [];
      for (const item of spineItems) {
        try {
          await item.load(book.load.bind(book));
          found.push(...item.find(q));
          item.unload();
        } catch (err) {}
        if (found.length > 200) break;
      }
      setResults(found);
      setResIdx(0);
      if (found.length && renditionRef.current) renditionRef.current.display(found[0].cfi);
    } finally {
      setSearching(false);
    }
  };

  const gotoResult = (delta) => {
    if (!results || !results.length) return;
    const next = (resIdx + delta + results.length) % results.length;
    setResIdx(next);
    if (renditionRef.current) renditionRef.current.display(results[next].cfi);
  };

  const clearSel = () => {
    if (renditionRef.current) {
      renditionRef.current.getContents().forEach((c) => { try { c.window.getSelection().removeAllRanges(); } catch (e) {} });
    }
    setSelection(null);
  };

  const highlight = (color) => {
    if (!selection || !renditionRef.current) return;
    try {
      renditionRef.current.annotations.add("highlight", selection.cfiRange, {}, undefined, "reader-highlight", { fill: color, "fill-opacity": "0.35" });
      A.addAnnotation(m.id, { kind: "epub", cfi: selection.cfiRange, color, text: selection.text.slice(0, 200) });
    } catch (e) {}
    clearSel();
  };

  return (
    <div className="reader-body">
      <div className="reader-toolbar">
        <div className="reader-tb__group">
          <button className={"reader-tb__btn" + (sidebarOpen ? " reader-tb__btn--on" : "")} onClick={() => setSidebarOpen((v) => !v)} title="Закладки (оглавление)"><Icon name="list" size={14} /></button>
        </div>
        <div className="reader-tb__group">
          <button className="reader-tb__btn" onClick={goPrev} title="Назад"><Icon name="back" size={14} /></button>
          <button className="reader-tb__btn" onClick={goNext} title="Вперёд"><Icon name="arrow" size={14} /></button>
          <span className="reader-tb__count">{m.progress.current || 0}%</span>
        </div>
        <div className="reader-tb__group">
          <button className="reader-tb__btn" onClick={() => setFontIdx((i) => (i + 1) % EPUB_FONT_SIZES.length)} title={"Размер шрифта: " + EPUB_FONT_SIZES[fontIdx] + "%"}>
            <span style={{ fontFamily: "var(--font-slab)", fontWeight: 700, fontSize: 13 }}>Aa</span>
          </button>
        </div>
        <form className="reader-tb__group" style={{ marginLeft: "auto" }} onSubmit={runSearch}>
          <span className="reader-tb__label">Найти:</span>
          <input className="reader-tb__search" value={query} onChange={(e) => setQuery(e.target.value)} placeholder={searching ? "ищу…" : ""} />
          <button className="reader-tb__btn" type="button" onClick={() => gotoResult(-1)} disabled={!results || !results.length} title="Предыдущее совпадение"><Icon name="back" size={12} /></button>
          <button className="reader-tb__btn" type="button" onClick={() => gotoResult(1)} disabled={!results || !results.length} title="Следующее совпадение"><Icon name="arrow" size={12} /></button>
          {results && <span className="reader-tb__count">{results.length ? (resIdx + 1) + " / " + results.length : "нет"}</span>}
        </form>
      </div>

      {loading && <div className="reader-loading">Открываю книгу…</div>}
      {error && <div className="reader-error">{error}</div>}
      <div className="reader-main">
        {sidebarOpen && (
          <ReaderSidebar items={toc} onClose={() => setSidebarOpen(false)}
            onGo={(it) => { if (renditionRef.current) renditionRef.current.display(it.href); }} />
        )}
        <div className="reader-content">
          <div ref={containerRef} className="reader-epub-view" />
        </div>
      </div>
      {selection && <SelectionToolbar x={selection.x} y={selection.y} onPick={highlight} onClose={clearSel} />}
    </div>
  );
}

/* ---- PDF reader (pdf.js) ----------------------------------------- */
const PDF_BASE_SCALE = 1.35;

function PdfReader({ m }) {
  const canvasRef = useRef(null);
  const textLayerRef = useRef(null);
  const overlayRef = useRef(null);
  const pdfRef = useRef(null);
  const saveTimer = useRef(null);
  const pageTextsRef = useRef({});
  const scrollRef = useRef(null);
  const [pageNum, setPageNum] = useState(Math.max(1, m.progress.current || 1));
  const [pageInput, setPageInput] = useState(String(Math.max(1, m.progress.current || 1)));
  const [numPages, setNumPages] = useState(m.progress.total || 0);
  const [scale, setScale] = useState(PDF_BASE_SCALE);
  /* "width" — вписать по ширине, "page" — страница целиком, "custom" — ручной зум */
  const [fitMode, setFitMode] = useState("width");
  const [pageSize, setPageSize] = useState({ w: 0, h: 0 });
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);
  const [selection, setSelection] = useState(null);
  const [pageAnnotations, setPageAnnotations] = useState([]);
  const [toc, setToc] = useState([]);
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [query, setQuery] = useState("");
  const [searchQ, setSearchQ] = useState("");     /* выполненный запрос */
  const [matches, setMatches] = useState(null);   /* массив номеров страниц (по совпадению на элемент) */
  const [matchIdx, setMatchIdx] = useState(0);
  const [searchHits, setSearchHits] = useState([]); /* прямоугольники совпадений на текущей странице */
  const [searching, setSearching] = useState(false);

  useEffect(() => { setPageInput(String(pageNum)); }, [pageNum]);

  /* авто-подгон масштаба под размер окна: страница занимает всю доступную
     область, а не висит маленькой в центре. Пересчитывается при смене
     страницы (страницы бывают разного формата) и при ресайзе контейнера. */
  useEffect(() => {
    if (fitMode === "custom" || !pdfRef.current || !scrollRef.current) return;
    let cancelled = false;
    const recalc = async () => {
      const doc = pdfRef.current, el = scrollRef.current;
      if (!doc || !el) return;
      const page = await doc.getPage(pageNum);
      if (cancelled) return;
      const vp = page.getViewport({ scale: 1 });
      const cs = getComputedStyle(el);
      const availW = el.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
      const availH = el.clientHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom);
      if (availW <= 0) return;
      let s = availW / vp.width;
      if (fitMode === "page" && availH > 0) s = Math.min(s, availH / vp.height);
      s = Math.max(0.2, Math.min(6, s));
      if (!cancelled) setScale(Number(s.toFixed(3)));
    };
    recalc();
    const ro = new ResizeObserver(recalc);
    ro.observe(scrollRef.current);
    return () => { cancelled = true; ro.disconnect(); };
  }, [fitMode, pageNum, loading]);

  useEffect(() => {
    let destroyed = false;
    (async () => {
      try {
        const res = await window.authFetch("/api/materials/" + m.id + "/file");
        if (!res.ok) throw new Error("Не удалось загрузить файл книги");
        const buf = await res.arrayBuffer();
        if (destroyed) return;
        const doc = await window.pdfjsLib.getDocument({
          data: buf,
          /* без standardFontDataUrl pdf.js навсегда зависает на страницах со стандартными шрифтами */
          standardFontDataUrl: "https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/standard_fonts/",
          cMapUrl: "https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/cmaps/",
          cMapPacked: true,
        }).promise;
        if (destroyed) return;
        pdfRef.current = doc;
        setNumPages(doc.numPages);

        try {
          const outline = await doc.getOutline();
          const flat = (items, depth = 0) => (items || []).flatMap((it) => [
            { title: it.title || "—", dest: it.dest, depth },
            ...flat(it.items, depth + 1),
          ]);
          setToc(flat(outline));
        } catch (e) {}

        setLoading(false);
      } catch (e) {
        if (!destroyed) { setError(e.message); setLoading(false); }
      }
    })();
    return () => { destroyed = true; clearTimeout(saveTimer.current); };
    // eslint-disable-next-line
  }, [m.id, m.file]);

  const renderTaskRef = useRef(null);

  useEffect(() => {
    if (!pdfRef.current) return;
    let cancelled = false;
    (async () => {
      try {
        const page = await pdfRef.current.getPage(pageNum);
        if (cancelled) return;
        const viewport = page.getViewport({ scale });
        const canvas = canvasRef.current;
        canvas.width = viewport.width; canvas.height = viewport.height;
        const ctx = canvas.getContext("2d");
        /* pdf.js запрещает два render() на одном canvas — отменяем предыдущий */
        if (renderTaskRef.current) { try { renderTaskRef.current.cancel(); } catch (e) {} }
        const task = page.render({ canvasContext: ctx, viewport });
        renderTaskRef.current = task;
        try { await task.promise; }
        catch (e) { if (e && e.name === "RenderingCancelledException") return; throw e; }
        finally { if (renderTaskRef.current === task) renderTaskRef.current = null; }
        if (cancelled) return;

      const textContent = await page.getTextContent();
      if (cancelled) return;
      const textLayerDiv = textLayerRef.current;
      textLayerDiv.innerHTML = "";
      textLayerDiv.style.width = viewport.width + "px";
      textLayerDiv.style.height = viewport.height + "px";
      textLayerDiv.style.setProperty("--scale-factor", scale);
      try {
        const task = window.pdfjsLib.renderTextLayer({ textContentSource: textContent, container: textLayerDiv, viewport, textDivs: [] });
        if (task && task.promise) await task.promise;
      } catch (e) { /* некоторые версии pdf.js используют другой параметр */ }
      if (cancelled) return;

      /* подсветка совпадений поиска на текущей странице */
      const hits = [];
      if (searchQ) {
        const q = searchQ.toLowerCase();
        const base = textLayerDiv.getBoundingClientRect();
        if (base.width > 0 && base.height > 0) {
          textLayerDiv.querySelectorAll("span").forEach((sp) => {
            if ((sp.textContent || "").toLowerCase().includes(q)) {
              const r = sp.getBoundingClientRect();
              hits.push({ x: (r.left - base.left) / base.width, y: (r.top - base.top) / base.height, w: r.width / base.width, h: r.height / base.height });
            }
          });
        }
      }
      setSearchHits(hits);

      setPageSize({ w: viewport.width, h: viewport.height });
      setPageAnnotations((m.annotations || []).filter((a) => a.kind === "pdf" && a.page === pageNum));

      clearTimeout(saveTimer.current);
      saveTimer.current = setTimeout(() => A.setReaderProgress(m.id, pageNum), 500);
      } catch (e) {
        console.error("PDF render error:", e);
      }
    })();
    return () => { cancelled = true; };
    // eslint-disable-next-line
  }, [pageNum, scale, searchQ, pdfRef.current]);

  const onMouseUp = () => {
    const sel = window.getSelection();
    if (!sel || sel.isCollapsed || !sel.toString().trim()) return;
    const range = sel.getRangeAt(0);
    const rects = Array.from(range.getClientRects()).filter((r) => r.width > 0 && r.height > 0);
    if (!rects.length) return;
    const containerRect = textLayerRef.current.getBoundingClientRect();
    const relRects = rects.map((r) => ({
      x: (r.left - containerRect.left) / containerRect.width,
      y: (r.top - containerRect.top) / containerRect.height,
      w: r.width / containerRect.width,
      h: r.height / containerRect.height,
    }));
    const mid = rects[Math.floor(rects.length / 2)];
    setSelection({ rects: relRects, text: sel.toString(), x: mid.left + mid.width / 2, y: mid.top });
  };

  const clearSel = () => { window.getSelection().removeAllRanges(); setSelection(null); };

  const highlight = (color) => {
    if (!selection) return;
    const ann = { kind: "pdf", page: pageNum, rects: selection.rects, color, text: selection.text.slice(0, 200) };
    A.addAnnotation(m.id, ann);
    setPageAnnotations((prev) => [...prev, ann]);
    clearSel();
  };

  const goPrev = () => setPageNum((n) => Math.max(1, n - 1));
  const goNext = () => setPageNum((n) => Math.min(numPages || n, n + 1));

  useEffect(() => {
    const onKey = (e) => {
      if (e.target && (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")) return;
      if (e.key === "ArrowLeft") goPrev();
      if (e.key === "ArrowRight") goNext();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [numPages]);

  const submitPage = (e) => {
    e.preventDefault();
    const n = parseInt(pageInput, 10);
    if (n >= 1 && n <= (numPages || 1)) setPageNum(n);
    else setPageInput(String(pageNum));
  };

  const goDest = async (dest) => {
    const doc = pdfRef.current;
    if (!doc || !dest) return;
    try {
      const d = typeof dest === "string" ? await doc.getDestination(dest) : dest;
      if (!d || !d[0]) return;
      const idx = await doc.getPageIndex(d[0]);
      setPageNum(idx + 1);
    } catch (e) {}
  };

  const runSearch = async (e) => {
    if (e) e.preventDefault();
    const q = query.trim().toLowerCase();
    const doc = pdfRef.current;
    if (!q || !doc) { setMatches(null); setSearchQ(""); return; }
    setSearching(true);
    try {
      const found = [];
      for (let p = 1; p <= doc.numPages; p++) {
        if (pageTextsRef.current[p] == null) {
          const page = await doc.getPage(p);
          const tc = await page.getTextContent();
          pageTextsRef.current[p] = tc.items.map((i) => i.str).join(" ").toLowerCase();
        }
        const text = pageTextsRef.current[p];
        let pos = text.indexOf(q);
        while (pos !== -1) { found.push(p); pos = text.indexOf(q, pos + q.length); }
      }
      setMatches(found);
      setMatchIdx(0);
      setSearchQ(q);
      if (found.length) setPageNum(found[0]);
    } finally {
      setSearching(false);
    }
  };

  const gotoMatch = (delta) => {
    if (!matches || !matches.length) return;
    const next = (matchIdx + delta + matches.length) % matches.length;
    setMatchIdx(next);
    setPageNum(matches[next]);
  };

  /* 100% = реальный размер страницы (как в Sumatra), а не внутренний базовый масштаб */
  const zoomPct = Math.round(scale * 100);
  const zoom = (dir) => {
    setFitMode("custom");
    setScale((s) => Math.max(0.2, Math.min(6, Math.round((s + dir * 0.15) * 100) / 100)));
  };

  return (
    <div className="reader-body">
      <div className="reader-toolbar">
        <div className="reader-tb__group">
          <button className={"reader-tb__btn" + (sidebarOpen ? " reader-tb__btn--on" : "")} onClick={() => setSidebarOpen((v) => !v)} title="Закладки (оглавление)"><Icon name="list" size={14} /></button>
        </div>
        <form className="reader-tb__group" onSubmit={submitPage}>
          <span className="reader-tb__label">Страница:</span>
          <input className="reader-tb__pageinput" value={pageInput} onChange={(e) => setPageInput(e.target.value)} />
          <span className="reader-tb__count">/ {numPages || "…"}</span>
        </form>
        <div className="reader-tb__group">
          <button className="reader-tb__btn" onClick={goPrev} title="Предыдущая страница"><Icon name="back" size={14} /></button>
          <button className="reader-tb__btn" onClick={goNext} title="Следующая страница"><Icon name="arrow" size={14} /></button>
        </div>
        <div className="reader-tb__group">
          <button className="reader-tb__btn" onClick={() => zoom(-1)} title="Уменьшить">−</button>
          <span className="reader-tb__count" style={{ textAlign: "center" }}>{zoomPct}%</span>
          <button className="reader-tb__btn" onClick={() => zoom(1)} title="Увеличить">+</button>
          <button className={"reader-tb__btn" + (fitMode === "width" ? " reader-tb__btn--on" : "")}
            onClick={() => setFitMode("width")} title="По ширине окна">⇔</button>
          <button className={"reader-tb__btn" + (fitMode === "page" ? " reader-tb__btn--on" : "")}
            onClick={() => setFitMode("page")} title="Страница целиком">⤢</button>
        </div>
        <form className="reader-tb__group" style={{ marginLeft: "auto" }} onSubmit={runSearch}>
          <span className="reader-tb__label">Найти:</span>
          <input className="reader-tb__search" value={query} onChange={(e) => setQuery(e.target.value)} placeholder={searching ? "ищу…" : ""} />
          <button className="reader-tb__btn" type="button" onClick={() => gotoMatch(-1)} disabled={!matches || !matches.length} title="Предыдущее совпадение"><Icon name="back" size={12} /></button>
          <button className="reader-tb__btn" type="button" onClick={() => gotoMatch(1)} disabled={!matches || !matches.length} title="Следующее совпадение"><Icon name="arrow" size={12} /></button>
          {matches && <span className="reader-tb__count">{matches.length ? (matchIdx + 1) + " / " + matches.length : "нет"}</span>}
        </form>
      </div>

      {loading && <div className="reader-loading">Открываю PDF…</div>}
      {error && <div className="reader-error">{error}</div>}
      <div className="reader-main">
        {sidebarOpen && <ReaderSidebar items={toc} onClose={() => setSidebarOpen(false)} onGo={(it) => goDest(it.dest)} />}
        <div className="reader-content">
          <div className="reader-pdf-scroll" ref={scrollRef}>
            <div className="reader-pdf-page" style={{ width: pageSize.w, height: pageSize.h }} onMouseUp={onMouseUp}>
              <canvas ref={canvasRef} />
              <div ref={textLayerRef} className="reader-pdf-textlayer" />
              <div ref={overlayRef} className="reader-pdf-overlay">
                {pageAnnotations.map((a, i) => (a.rects || []).map((r, j) => (
                  <div key={i + "-" + j} className="reader-pdf-highlight"
                    style={{ left: r.x * 100 + "%", top: r.y * 100 + "%", width: r.w * 100 + "%", height: r.h * 100 + "%", background: a.color }} />
                )))}
                {searchHits.map((r, i) => (
                  <div key={"s" + i} className="reader-search-hitbox"
                    style={{ left: r.x * 100 + "%", top: r.y * 100 + "%", width: r.w * 100 + "%", height: r.h * 100 + "%" }} />
                ))}
              </div>
            </div>
          </div>
        </div>
      </div>
      {selection && <SelectionToolbar x={selection.x} y={selection.y} onPick={highlight} onClose={clearSel} />}
    </div>
  );
}

/* ---- DOC/DOCX reader (сконвертирован в HTML на сервере) --------- */
const HTML_FONT_SIZES = [88, 100, 112, 126];

function HtmlReader({ m }) {
  const [html, setHtml] = useState(m.docHtml || null);
  const containerRef = useRef(null);
  const scrollTimer = useRef(null);
  const restoredScroll = useRef(false);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(!m.docHtml);
  const [selection, setSelection] = useState(null);
  const [fontIdx, setFontIdx] = useState(1);

  useEffect(() => {
    if (m.docHtml) { setHtml(m.docHtml); setLoading(false); return; }
    let destroyed = false;
    (async () => {
      try {
        const res = await window.authFetch("/api/materials/" + m.id + "/file");
        if (!res.ok) throw new Error("Не удалось загрузить файл документа");
        const text = await res.text();
        if (destroyed) return;
        setHtml(text);
        setLoading(false);
      } catch (e) {
        if (!destroyed) { setError(e.message); setLoading(false); }
      }
    })();
    return () => { destroyed = true; clearTimeout(scrollTimer.current); };
    // eslint-disable-next-line
  }, [m.id, m.file]);

  useEffect(() => {
    if (!html || !containerRef.current || restoredScroll.current) return;
    restoredScroll.current = true;
    const el = containerRef.current;
    const pct = (m.progress.current || 0) / 100;
    requestAnimationFrame(() => { el.scrollTop = pct * Math.max(0, el.scrollHeight - el.clientHeight); });
  }, [html]);

  const onScroll = () => {
    const el = containerRef.current;
    const max = el.scrollHeight - el.clientHeight;
    const pct = max > 0 ? Math.round((el.scrollTop / max) * 100) : 0;
    clearTimeout(scrollTimer.current);
    scrollTimer.current = setTimeout(() => A.setReaderProgress(m.id, pct), 500);
  };

  const onMouseUp = () => {
    const sel = window.getSelection();
    if (!sel || sel.isCollapsed || !sel.toString().trim()) return;
    if (!containerRef.current.contains(sel.anchorNode)) return;
    const range = sel.getRangeAt(0);
    const rect = range.getBoundingClientRect();
    setSelection({ range: range.cloneRange(), text: sel.toString(), x: rect.left + rect.width / 2, y: rect.top });
  };

  const highlight = (color) => {
    if (!selection) return;
    try {
      const mark = document.createElement("mark");
      mark.style.background = color;
      mark.className = "reader-html-mark";
      const content = selection.range.extractContents();
      mark.appendChild(content);
      selection.range.insertNode(mark);
      window.getSelection().removeAllRanges();
      const newHtml = containerRef.current.innerHTML;
      A.updateMaterial(m.id, { docHtml: newHtml });
      A.addAnnotation(m.id, { kind: "html", color, text: selection.text.slice(0, 200) });
    } catch (e) { /* выделение зацепило несколько несовместимых узлов — пропускаем */ }
    setSelection(null);
  };

  return (
    <div className="reader-body">
      <div className="reader-toolbar">
        <div className="reader-tb__group">
          <button className="reader-tb__btn" onClick={() => setFontIdx((i) => (i + 1) % HTML_FONT_SIZES.length)} title={"Размер шрифта: " + HTML_FONT_SIZES[fontIdx] + "%"}>
            <span style={{ fontFamily: "var(--font-slab)", fontWeight: 700, fontSize: 13 }}>Aa</span>
          </button>
          <span className="reader-tb__count">{HTML_FONT_SIZES[fontIdx]}%</span>
        </div>
        <span className="reader-tb__count" style={{ marginLeft: "auto" }}>{m.progress.current || 0}%</span>
      </div>
      {loading && <div className="reader-loading">Открываю документ…</div>}
      {error && <div className="reader-error">{error}</div>}
      <div ref={containerRef} className="reader-html-view" style={{ fontSize: HTML_FONT_SIZES[fontIdx] + "%" }}
        onMouseUp={onMouseUp} onScroll={onScroll}
        dangerouslySetInnerHTML={{ __html: html || "" }} />
      {selection && <SelectionToolbar x={selection.x} y={selection.y} onPick={highlight} onClose={() => setSelection(null)} />}
    </div>
  );
}

/* ---- entry screen: upload prompt or dispatch to the right engine  */
function ReaderScreen({ state, id, nav }) {
  const Cmp = window;
  const m = state.materials.find((x) => x.id === id);
  const fileInputRef = useRef(null);
  const [uploading, setUploading] = useState(false);
  const [uploadError, setUploadError] = useState(null);

  if (!m) return <div className="page"><div className="empty">Материал не найден.</div></div>;

  const pickFile = () => fileInputRef.current && fileInputRef.current.click();
  const onFileChange = async (e) => {
    const file = e.target.files[0];
    if (!file) return;
    setUploading(true); setUploadError(null);
    try {
      const form = new FormData();
      form.append("file", file);
      const res = await window.authFetch("/api/materials/" + m.id + "/upload", { method: "POST", body: form });
      const d = await res.json();
      if (!res.ok) throw new Error(d.error || "не удалось загрузить файл");
      A.setMaterialFile(m.id, d);
    } catch (err) {
      setUploadError(err.message);
    } finally {
      setUploading(false);
      e.target.value = "";
    }
  };

  const fileInput = <input ref={fileInputRef} type="file" accept=".pdf,.epub,.fb2,.doc,.docx" style={{ display: "none" }} onChange={onFileChange} />;

  if (!m.file) {
    return (
      <div className="page screen-enter" style={{ maxWidth: 640 }}>
        <div className="row-3" style={{ marginBottom: "var(--s-5)" }}>
          <span className="task__link" onClick={() => nav({ screen: "material", id: m.id })}><Icon name="back" size={14} />{m.title}</span>
        </div>
        <div className="empty" style={{ padding: "var(--s-7)", textAlign: "center" }}>
          <div style={{ marginBottom: 16 }}>Файл ещё не загружен. Поддерживаются PDF, EPUB, FB2, DOC, DOCX.</div>
          <Cmp.GButton variant="ink" icon="upload" onClick={pickFile}>{uploading ? "Загружаю…" : "Загрузить файл"}</Cmp.GButton>
          {fileInput}
          {uploadError && <div style={{ color: "var(--signal)", marginTop: 12, fontSize: "var(--t-small)" }}>{uploadError}</div>}
        </div>
      </div>
    );
  }

  return (
    <div className="reader-page">
      <div className="reader-topbar">
        <span className="task__link" onClick={() => nav({ screen: "material", id: m.id })}><Icon name="back" size={14} />{m.title}</span>
        <span className="e-label reader-topbar__progress">
          {m.progress.unit === "%" ? m.progress.current + "%" : m.progress.current + " / " + m.progress.total + " " + m.progress.unit}
        </span>
        <span className="task__link" onClick={pickFile} style={{ fontSize: "var(--t-small)" }}>Заменить файл</span>
        {fileInput}
      </div>
      {uploadError && <div className="reader-error">{uploadError}</div>}
      {m.fileType === "epub" && <EpubReader key={m.file} m={m} />}
      {m.fileType === "pdf" && <PdfReader key={m.file} m={m} />}
      {m.fileType === "html" && <HtmlReader key={m.file} m={m} />}
    </div>
  );
}

window.Screens = Object.assign(window.Screens || {}, { reader: ReaderScreen });
