/* ============================================================
   custom-quests.jsx — раздел «Свои квесты».
   Пользователь создаёт собственные квесты: название, описание,
   запись действий и учёт времени с той же ступенчатой шкалой
   баллов, что и у курсов (за 1-й час / часы 2–10 / далее).
   Регистрирует window.Screens.questsCustom (вкладка в «Квестах»).
   ============================================================ */
const React = window.React;
const { useState, useEffect, useRef } = React;
const A = window.PlannerActions;
const Icon = window.Icon;

/* Картинка заметки: грузится через authFetch (папка защищена токеном) в
   blob-URL, показывается миниатюрой, по клику открывается в полном размере. */
function NoteImage({ name, onRemove }) {
  const [url, setUrl] = useState(null);
  const [err, setErr] = useState(false);
  useEffect(() => {
    let dead = false, obj = null;
    window.authFetch(window.noteImageUrl(name))
      .then((r) => { if (!r.ok) throw new Error("404"); return r.blob(); })
      .then((b) => { if (dead) return; obj = URL.createObjectURL(b); setUrl(obj); })
      .catch(() => { if (!dead) setErr(true); });
    return () => { dead = true; if (obj) URL.revokeObjectURL(obj); };
  }, [name]);
  return (
    <div className="note-img">
      {url ? (
        <a href={url} target="_blank" rel="noreferrer"><img src={url} alt="" /></a>
      ) : (
        <div className="note-img__ph">{err ? "✕" : "…"}</div>
      )}
      {onRemove && <button className="note-img__del" onClick={onRemove} title="Убрать"><Icon name="close" size={11} /></button>}
    </div>
  );
}

function QuestRatesEditor({ rates, onChange }) {
  const set = (k, v) => onChange({ ...rates, [k]: Math.max(0, Math.round(Number(v) || 0)) });
  const field = (k, label) => (
    <label style={{ display: "flex", flexDirection: "column", gap: 4 }}>
      <span className="e-label">{label}</span>
      <input type="number" min="0" value={rates[k]} onChange={(e) => set(k, e.target.value)}
        style={{ width: 76, border: "1px solid var(--ink-40)", padding: "6px 8px", fontFamily: "var(--font-mono)", background: "var(--paper-00)" }} />
    </label>
  );
  return (
    <div className="row-3 wrap gap-3">
      {field("firstHour", "За 1-й час")}
      {field("first10h", "Часы 2–10")}
      {field("normal", "Далее (10+)")}
    </div>
  );
}

function CustomQuestCard({ quest, state, nav, timer, todayIdx }) {
  const Cmp = window;
  const [editing, setEditing] = useState(false);
  const [eTitle, setETitle] = useState(quest.title);
  const [eDesc, setEDesc] = useState(quest.desc || "");
  const [eRates, setERates] = useState(quest.ptsRates || Cmp.DEFAULT_RATES);
  const [actionDraft, setActionDraft] = useState("");
  const [linking, setLinking] = useState(false);
  const [linkQuery, setLinkQuery] = useState("");

  const mats = (state && state.materials) || [];
  const linked = (quest.links || []).map((id) => mats.find((m) => m.id === id)).filter(Boolean);
  const lq = linkQuery.trim().toLowerCase();
  const results = lq ? mats.filter((m) => ((m.title || "") + " " + (m.author || "")).toLowerCase().includes(lq)).slice(0, 40) : [];
  const openMat = (id) => nav && nav({ screen: "material", id });
  const [draftImages, setDraftImages] = useState([]); /* имена уже загруженных картинок черновика */
  const [imgBusy, setImgBusy] = useState(0);
  const fileRef = useRef(null);

  const uploadImages = async (files) => {
    const imgs = Array.from(files || []).filter((f) => f && /^image\//.test(f.type));
    if (!imgs.length) return;
    for (const f of imgs) {
      setImgBusy((n) => n + 1);
      try {
        const name = await window.uploadNoteImage(f);
        setDraftImages((arr) => [...arr, name]);
      } catch (e) {
        window.pushToast && window.pushToast({ id: Date.now() + Math.random(), text: "Картинка: " + e.message, pts: 0 });
      } finally {
        setImgBusy((n) => n - 1);
      }
    }
  };
  const onPaste = (e) => {
    const items = (e.clipboardData && e.clipboardData.items) || [];
    const files = [];
    for (const it of items) { if (it.kind === "file" && /^image\//.test(it.type)) { const f = it.getAsFile(); if (f) files.push(f); } }
    if (files.length) { e.preventDefault(); uploadImages(files); }
  };
  const onPickFiles = (e) => { uploadImages(e.target.files); e.target.value = ""; };

  const allSec = (quest.timeLog || []).reduce((s, e) => s + Cmp.entrySec(e), 0);
  const totalPts = Cmp.courseTimePts(allSec, quest.ptsRates);
  const todayPts = Cmp.cquestTimePtsToday(quest);
  const r = quest.ptsRates || Cmp.DEFAULT_RATES;

  const save = () => {
    A.updateCustomQuest(quest.id, { title: eTitle.trim() || quest.title, desc: eDesc.trim(), ptsRates: eRates });
    setEditing(false);
  };
  const addAction = () => {
    if (!actionDraft.trim() && !draftImages.length) return;
    A.addQuestAction(quest.id, actionDraft, draftImages);
    setActionDraft(""); setDraftImages([]);
  };

  return (
    <div className="task" style={{ display: "block", padding: "var(--s-5)", marginBottom: "var(--s-5)" }}>
      {editing ? (
        <div>
          <div className="e-label" style={{ marginBottom: 6 }}>Название</div>
          <input value={eTitle} onChange={(e) => setETitle(e.target.value)} className="g-input" style={{ width: "100%", marginBottom: 10 }} />
          <div className="e-label" style={{ marginBottom: 6 }}>Описание</div>
          <textarea value={eDesc} onChange={(e) => setEDesc(e.target.value)} rows={2}
            style={{ width: "100%", border: "1px solid var(--ink-40)", background: "var(--paper-00)", padding: "8px 10px", fontFamily: "var(--font-slab)", fontSize: "var(--t-body)", resize: "vertical", boxSizing: "border-box", marginBottom: 10 }} />
          <div className="e-label" style={{ marginBottom: 6 }}>Баллы за время</div>
          <QuestRatesEditor rates={eRates} onChange={setERates} />
          <div className="row-2 gap-3" style={{ marginTop: 12 }}>
            <Cmp.GButton variant="signal" icon="check" onClick={save}>Сохранить</Cmp.GButton>
            <Cmp.GButton variant="ghost" onClick={() => setEditing(false)}>Отмена</Cmp.GButton>
          </div>
        </div>
      ) : (
        <>
          <div className="row-3" style={{ justifyContent: "space-between", alignItems: "flex-start", gap: 12 }}>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div className="module__title" style={{ fontSize: "var(--t-body-lg)" }}>{quest.title}</div>
              {quest.desc && <div className="task__desc-preview" style={{ marginTop: 4 }}>{quest.desc}</div>}
            </div>
            <div className="row-3" style={{ gap: 14, flex: "0 0 auto" }}>
              <span className="task__link" onClick={() => { setETitle(quest.title); setEDesc(quest.desc || ""); setERates(quest.ptsRates || Cmp.DEFAULT_RATES); setEditing(true); }} style={{ fontSize: "var(--t-small)" }}><Icon name="edit" size={13} />Изменить</span>
              <span className="task__link" onClick={() => { if (confirm("Удалить квест «" + quest.title + "»?")) A.removeCustomQuest(quest.id); }} style={{ fontSize: "var(--t-small)", color: "var(--signal)" }}><Icon name="close" size={13} />Удалить</span>
            </div>
          </div>

          <div className="row-3 wrap gap-4" style={{ margin: "var(--s-4) 0", alignItems: "baseline" }}>
            <span className="task__pts pts-pos">+{todayPts} PTS сегодня</span>
            <span className="e-label">всего {totalPts} PTS · {Cmp.fmtDur(allSec)}</span>
            <span className="e-label" style={{ color: "var(--text-faint)" }}>ставка: {r.firstHour} / {r.first10h} / {r.normal} б/ч</span>
          </div>

          <Cmp.TimeBlock log={quest.timeLog} todayIdx={todayIdx} timer={timer} kind="cquest" id={quest.id} label="Время квеста"
            onStart={(k, i) => A.startTimer("cquest", quest.id)} onStop={() => A.stopTimer()}
            onAdd={(sec, dateISO) => A.logTime("cquest", quest.id, sec, null, dateISO)} />

          <div style={{ marginTop: "var(--s-4)" }}>
            <Cmp.SectionRule kicker="Материалы"
              right={<span className="task__link" onClick={() => setLinking((v) => !v)} style={{ fontSize: "var(--t-small)" }}><Icon name="link" size={12} /> привязать книги</span>}>
              Книги квеста
            </Cmp.SectionRule>
            {linked.length === 0 && !linking && (
              <div className="muted" style={{ fontSize: "var(--t-small)", marginTop: 6 }}>Пока нет привязанных книг. Нажмите «привязать книги».</div>
            )}
            {linked.length > 0 && (
              <div className="qbooks">
                {linked.map((m) => (
                  <div key={m.id} className="qbook">
                    <Icon name={m.type === "video" ? "film" : "book"} size={14} />
                    <span className="qbook__title" onClick={() => openMat(m.id)} title="Открыть материал">{m.title}</span>
                    {m.author && <span className="qbook__author">{m.author}</span>}
                    {m.file && <span className="qbook__read" onClick={() => nav && nav({ screen: "reader", id: m.id })}>читать</span>}
                    <button className="qbook__del" title="Отвязать" onClick={() => A.toggleQuestLink(quest.id, m.id)}><Icon name="close" size={12} /></button>
                  </div>
                ))}
              </div>
            )}
            {linking && (
              <div className="qbook-picker">
                <input autoFocus value={linkQuery} onChange={(e) => setLinkQuery(e.target.value)}
                  placeholder="Поиск книги по названию или автору…" className="qbook-picker__input" />
                <div className="qbook-picker__list">
                  {!lq && <div className="muted" style={{ fontSize: "var(--t-small)" }}>Начните вводить название или автора…</div>}
                  {lq && results.length === 0 && <div className="muted" style={{ fontSize: "var(--t-small)" }}>Ничего не найдено.</div>}
                  {results.map((m) => {
                    const on = (quest.links || []).includes(m.id);
                    return (
                      <div key={m.id} className="qbook-opt" onClick={() => A.toggleQuestLink(quest.id, m.id)}>
                        <span className={"qbook-opt__box" + (on ? " qbook-opt__box--on" : "")}>{on && <Icon name="check" size={11} style={{ color: "#fff" }} />}</span>
                        <span className="qbook-opt__title">{m.title}</span>
                        {m.author && <span className="qbook-opt__author">{m.author}</span>}
                      </div>
                    );
                  })}
                </div>
                <div className="row-2" style={{ marginTop: 8 }}>
                  <Cmp.GButton variant="ghost" onClick={() => { setLinking(false); setLinkQuery(""); }} style={{ fontSize: "var(--t-small)", padding: "4px 12px" }}>Готово</Cmp.GButton>
                </div>
              </div>
            )}
          </div>

          <div style={{ marginTop: "var(--s-4)" }}>
            <Cmp.SectionRule kicker="Журнал">Запись действий</Cmp.SectionRule>
            <div className="row-3" style={{ gap: 8 }}>
              <input value={actionDraft} onChange={(e) => setActionDraft(e.target.value)} onPaste={onPaste}
                onKeyDown={(e) => e.key === "Enter" && addAction()} placeholder="Что сделал? (можно вставить картинку Ctrl+V)"
                style={{ flex: 1, border: "1px solid var(--ink-40)", background: "var(--paper-00)", padding: "8px 10px", fontFamily: "var(--font-slab)", fontSize: "var(--t-body)", outline: "none" }} />
              <button className="note-img-btn" onClick={() => fileRef.current && fileRef.current.click()} title="Добавить картинку">
                <Icon name="image" size={16} />
              </button>
              <input ref={fileRef} type="file" accept="image/*" multiple style={{ display: "none" }} onChange={onPickFiles} />
              <Cmp.GButton variant="ink" icon="plus" onClick={addAction}>Добавить</Cmp.GButton>
            </div>
            {(draftImages.length > 0 || imgBusy > 0) && (
              <div className="note-img-row" style={{ marginTop: 8 }}>
                {draftImages.map((name) => (
                  <NoteImage key={name} name={name} onRemove={() => setDraftImages((arr) => arr.filter((n) => n !== name))} />
                ))}
                {imgBusy > 0 && <div className="note-img__ph">…</div>}
              </div>
            )}
            {(quest.actions || []).length > 0 && (
              <div className="task__notes-list" style={{ marginTop: 10 }}>
                {(quest.actions || []).map((a) => (
                  <div key={a.id} className="task__note-item">
                    <span className="task__note-date">{a.date}</span>
                    <span className="task__note-text">
                      {a.text}
                      {(a.images || []).length > 0 && (
                        <div className="note-img-row" style={{ marginTop: a.text ? 6 : 0 }}>
                          {(a.images || []).map((name) => <NoteImage key={name} name={name} />)}
                        </div>
                      )}
                    </span>
                    <button className="task__note-remove" onClick={() => A.removeQuestAction(quest.id, a.id)} title="Удалить"><Icon name="close" size={12} /></button>
                  </div>
                ))}
              </div>
            )}
          </div>
        </>
      )}
    </div>
  );
}

/* Страница одного квеста (открывается со своей вкладки наверху) */
function CustomQuestPage({ questId, nav }) {
  const state = window.useStore();
  const quest = (state.customQuests || []).find((q) => q.id === questId);
  if (!quest) return <div className="empty" style={{ padding: "var(--s-7)" }}>Квест не найден.</div>;
  return <CustomQuestCard quest={quest} state={state} nav={nav} timer={state.timer} todayIdx={state.todayIdx} />;
}

/* Экран создания нового квеста (вкладка «Новый квест») */
function CustomQuestCreate({ onCreated }) {
  const Cmp = window;
  const [title, setTitle] = useState("");
  const [desc, setDesc] = useState("");
  const [rates, setRates] = useState({ ...Cmp.DEFAULT_RATES });
  const create = () => {
    if (!title.trim()) return;
    const id = A.addCustomQuest({ title, desc, ptsRates: rates });
    setTitle(""); setDesc(""); setRates({ ...Cmp.DEFAULT_RATES });
    if (onCreated && id) onCreated(id);
  };
  return (
    <div className="task" style={{ display: "block", padding: "var(--s-5)", maxWidth: 560 }}>
      <Cmp.SectionRule kicker="Свои квесты">Новый квест</Cmp.SectionRule>
      <div className="e-label" style={{ margin: "var(--s-4) 0 6px" }}>Название</div>
      <input value={title} onChange={(e) => setTitle(e.target.value)} autoFocus className="g-input" placeholder="Например: Тренировки"
        onKeyDown={(e) => e.key === "Enter" && create()} style={{ width: "100%", marginBottom: 10 }} />
      <div className="e-label" style={{ marginBottom: 6 }}>Описание</div>
      <textarea value={desc} onChange={(e) => setDesc(e.target.value)} rows={2} placeholder="Необязательно"
        style={{ width: "100%", border: "1px solid var(--ink-40)", background: "var(--paper-00)", padding: "8px 10px", fontFamily: "var(--font-slab)", fontSize: "var(--t-body)", resize: "vertical", boxSizing: "border-box", marginBottom: 10 }} />
      <div className="e-label" style={{ marginBottom: 6 }}>Баллы за время (за час)</div>
      <QuestRatesEditor rates={rates} onChange={setRates} />
      <div className="row-2 gap-3" style={{ marginTop: 14 }}>
        <Cmp.GButton variant="signal" icon="check" onClick={create}>Создать квест</Cmp.GButton>
      </div>
    </div>
  );
}

/* ---- Квест «Книги»: вертикальный чек-лист с трёхпозиционной отметкой,
   перетаскиванием, удалением и добавлением своих пунктов --------- */
const BOOK_MARK_CLASS = ["empty", "yellow", "green"];

/* Привязка пункта к книге из библиотеки: поиск по названию/автору. */
function BookLibraryLink({ listId, it, state, nav }) {
  const [picking, setPicking] = useState(false);
  const [q, setQ] = useState("");
  const mats = (state && state.materials) || [];
  const linked = it.link ? mats.find((m) => m.id === it.link) : null;
  const lq = q.trim().toLowerCase();
  const results = lq ? mats.filter((m) => ((m.title || "") + " " + (m.author || "")).toLowerCase().includes(lq)).slice(0, 30) : [];
  return (
    <div className="book-detail-row">
      <span className="e-label">Из библиотеки:</span>
      {linked ? (
        <>
          <span className="book-link__title" onClick={() => nav && nav({ screen: "material", id: linked.id })} title="Открыть материал">{linked.title}</span>
          {linked.file && <span className="book-link__read" onClick={() => nav && nav({ screen: "reader", id: linked.id })}>читать</span>}
          <button className="book-row__btn" onClick={() => A.setBookItemLink(listId, it.id, null)} title="Отвязать"><Icon name="close" size={12} /></button>
        </>
      ) : picking ? (
        <div className="book-link-picker">
          <input autoFocus value={q} onChange={(e) => setQ(e.target.value)} onKeyDown={(e) => e.key === "Escape" && setPicking(false)}
            placeholder="Поиск книги в библиотеке…" className="book-link-picker__input" />
          <div className="book-link-picker__list">
            {!lq && <div className="muted" style={{ fontSize: "var(--t-small)" }}>Начните вводить название или автора…</div>}
            {lq && results.length === 0 && <div className="muted" style={{ fontSize: "var(--t-small)" }}>Ничего не найдено.</div>}
            {results.map((m) => (
              <div key={m.id} className="qbook-opt" onClick={() => { A.setBookItemLink(listId, it.id, m.id); setPicking(false); setQ(""); }}>
                <span className="qbook-opt__title">{m.title}</span>
                {m.author && <span className="qbook-opt__author">{m.author}</span>}
              </div>
            ))}
          </div>
          <span className="task__link" style={{ fontSize: "var(--t-small)" }} onClick={() => setPicking(false)}>отмена</span>
        </div>
      ) : (
        <span className="task__link" style={{ fontSize: "var(--t-small)" }} onClick={() => setPicking(true)}><Icon name="link" size={12} /> привязать книгу из библиотеки</span>
      )}
    </div>
  );
}

/* нормализация глав для отображения (старый формат — массив чисел) */
function normChapters(chs) {
  return (chs || []).map((c, i) => typeof c === "number"
    ? { id: "idx" + i, mark: c }
    : { id: c.id || ("idx" + i), title: c.title, mark: c.mark || 0, note: c.note });
}

/* Одна глава: отметка, название (нумерованная или своя), заметка, перенос, удаление. */
function ChapterRow({ listId, bookId, ch, name, dragCh, overCh, setDragCh, setOverCh }) {
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(ch.title || "");
  const [noteOpen, setNoteOpen] = useState(false);
  const [noteDraft, setNoteDraft] = useState(ch.note || "");
  useEffect(() => { setNoteDraft(ch.note || ""); }, [ch.note]);
  const saveName = () => { A.renameBookChapter(listId, bookId, ch.id, draft); setEditing(false); };
  const saveNote = () => A.setBookChapterNote(listId, bookId, ch.id, noteDraft);
  return (
    <div className="chapter-row">
      <div className={"chapter-row__main" + (overCh === ch.id && dragCh && dragCh !== ch.id ? " chapter-row__main--over" : "") + (dragCh === ch.id ? " chapter-row__main--drag" : "")}
        draggable={!editing}
        onDragStart={(e) => { setDragCh(ch.id); try { e.dataTransfer.effectAllowed = "move"; } catch (er) {} }}
        onDragOver={(e) => { e.preventDefault(); if (overCh !== ch.id) setOverCh(ch.id); }}
        onDragLeave={() => { if (overCh === ch.id) setOverCh(null); }}
        onDrop={(e) => { e.preventDefault(); if (dragCh && dragCh !== ch.id) A.reorderBookChapter(listId, bookId, dragCh, ch.id); setDragCh(null); setOverCh(null); }}
        onDragEnd={() => { setDragCh(null); setOverCh(null); }}>
        <span className="chapter-row__handle" title="Перетащите, чтобы переместить">⠿</span>
        <button className={"book-mark book-mark--sm book-mark--" + BOOK_MARK_CLASS[ch.mark || 0]}
          onClick={() => A.cycleBookChapterMark(listId, bookId, ch.id)} title="Отметка главы: пусто → жёлтый → зелёный" />
        {editing ? (
          <input className="chapter-row__edit" autoFocus value={draft} onChange={(e) => setDraft(e.target.value)}
            onKeyDown={(e) => { if (e.key === "Enter") saveName(); if (e.key === "Escape") setEditing(false); }} onBlur={saveName} />
        ) : (
          <span className="chapter-row__name" onDoubleClick={() => { setDraft(ch.title || ""); setEditing(true); }} title="Двойной клик — переименовать">{name}</span>
        )}
        <button className={"chapter-row__notebtn" + (ch.note ? " chapter-row__notebtn--on" : "") + (noteOpen ? " chapter-row__notebtn--open" : "")}
          onClick={() => setNoteOpen((v) => !v)} title={ch.note ? "Заметка (есть)" : "Заметка к главе"}><Icon name="edit" size={12} /></button>
        <button className="book-row__btn" onClick={() => A.moveBookChapter(listId, bookId, ch.id, -1)} title="Выше">↑</button>
        <button className="book-row__btn" onClick={() => A.moveBookChapter(listId, bookId, ch.id, 1)} title="Ниже">↓</button>
        <button className="book-row__btn book-row__del" onClick={() => A.removeBookChapter(listId, bookId, ch.id)} title="Удалить главу"><Icon name="close" size={12} /></button>
      </div>
      {noteOpen && (
        <textarea className="chapter-row__note" value={noteDraft} placeholder="Заметка к этой главе…"
          onChange={(e) => setNoteDraft(e.target.value)} onBlur={saveNote} rows={2} />
      )}
    </div>
  );
}

/* Разворачиваемая панель: привязка к библиотеке + главы. */
function BookDetails({ listId, it, state, nav }) {
  const chapters = normChapters(it.chapters);
  const [n, setN] = useState("");
  const [newChap, setNewChap] = useState("");
  const [dragCh, setDragCh] = useState(null);
  const [overCh, setOverCh] = useState(null);
  const apply = () => { A.setBookChapters(listId, it.id, parseInt(n, 10) || 0); setN(""); };
  const addNamed = () => { if (newChap.trim()) { A.addBookChapter(listId, it.id, newChap.trim()); setNewChap(""); } };
  const doneCount = chapters.filter((c) => c.mark === 2).length;
  let num = 0;
  return (
    <div className="book-chapters">
      <BookLibraryLink listId={listId} it={it} state={state} nav={nav} />
      <div className="book-chapters__ctl">
        <span className="e-label">Нумерованных глав:</span>
        <input className="book-chapters__count" type="number" min="0" value={n} placeholder={String(chapters.filter((c) => !c.title).length || 0)}
          onChange={(e) => setN(e.target.value)}
          onKeyDown={(e) => { if (e.key === "Enter") apply(); }} />
        <button className="book-chapters__set" onClick={apply}>задать</button>
        {chapters.length > 0 && <span className="e-label" style={{ color: "var(--text-faint)" }}>прочитано {doneCount}/{chapters.length}</span>}
      </div>
      <div className="book-chapters__ctl">
        <span className="e-label">Своя глава:</span>
        <input className="book-chapters__name" value={newChap} placeholder="Напр. Введение, Предисловие…"
          onChange={(e) => setNewChap(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") addNamed(); }} />
        <button className="book-chapters__set" onClick={addNamed}>добавить</button>
      </div>
      {chapters.length > 0 && (
        <div className="book-chapters__list">
          {chapters.map((c) => (
            <ChapterRow key={c.id} listId={listId} bookId={it.id} ch={c} name={c.title || ("Глава " + (++num))}
              dragCh={dragCh} overCh={overCh} setDragCh={setDragCh} setOverCh={setOverCh} />
          ))}
        </div>
      )}
    </div>
  );
}

function BookRow({ listId, it, idx, open, onToggle, state, nav, onUnassign, dragId, overId, setDragId, setOverId }) {
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(it.text);
  const save = () => { A.renameBookItem(listId, it.id, draft); setEditing(false); };
  const chCount = (it.chapters || []).length;
  const chDone = (it.chapters || []).filter((c) => (typeof c === "number" ? c : (c && c.mark)) === 2).length;
  const linked = it.link && state ? (state.materials || []).find((m) => m.id === it.link) : null;
  return (
    <div className={"book-item" + (open ? " book-item--open" : "")}>
      <div
        className={"book-row" + (overId === it.id && dragId && dragId !== it.id ? " book-row--over" : "") + (dragId === it.id ? " book-row--drag" : "")}
        draggable={!editing}
        onDragStart={(e) => { setDragId(it.id); try { e.dataTransfer.effectAllowed = "move"; } catch (er) {} }}
        onDragOver={(e) => { e.preventDefault(); if (overId !== it.id) setOverId(it.id); }}
        onDragLeave={() => { if (overId === it.id) setOverId(null); }}
        onDrop={(e) => { e.preventDefault(); if (dragId && dragId !== it.id) A.reorderBookList(listId, dragId, it.id); setDragId(null); setOverId(null); }}
        onDragEnd={() => { setDragId(null); setOverId(null); }}>
        <span className="book-row__handle" title="Перетащите, чтобы переместить">⠿</span>
        <button className={"book-row__chevron" + (open ? " book-row__chevron--open" : "")} onClick={() => onToggle(it.id)} title={open ? "Свернуть" : "Развернуть (библиотека, главы)"}>▸</button>
        <button className={"book-mark book-mark--" + BOOK_MARK_CLASS[it.mark || 0]} onClick={() => A.cycleBookMark(listId, it.id)}
          title="Отметка: пусто → жёлтый → зелёный" />
        <span className="book-row__num">{idx + 1}</span>
        {editing ? (
          <input className="book-row__edit" autoFocus value={draft} onChange={(e) => setDraft(e.target.value)}
            onKeyDown={(e) => { if (e.key === "Enter") save(); if (e.key === "Escape") setEditing(false); }} onBlur={save} />
        ) : (
          <span className="book-row__text" onDoubleClick={() => { setDraft(it.text); setEditing(true); }} title="Двойной клик — переименовать">{it.text}</span>
        )}
        {linked && <span className="book-row__read" title={linked.title}
          onClick={() => nav && nav({ screen: linked.file ? "reader" : "material", id: linked.id })}><Icon name="book" size={12} /> читать</span>}
        {chCount > 0 && <span className="book-row__chaptertag" onClick={() => onToggle(it.id)} title="Главы">{chDone}/{chCount} гл.</span>}
        <button className="book-row__btn" onClick={() => A.moveBookItem(listId, it.id, -1)} title="Выше">↑</button>
        <button className="book-row__btn" onClick={() => A.moveBookItem(listId, it.id, 1)} title="Ниже">↓</button>
        {onUnassign && <button className="book-row__btn" onClick={onUnassign} title="Убрать из категории"><Icon name="back" size={13} /></button>}
        <button className="book-row__btn book-row__del" onClick={() => A.removeBookItem(listId, it.id)} title="Удалить из списка"><Icon name="close" size={13} /></button>
      </div>
      {open && <BookDetails listId={listId} it={it} state={state} nav={nav} />}
    </div>
  );
}

/* Заголовок категории. Категория — отдельная сущность (не позиция в списке).
   Книги входят в неё явно (поле b.cat). ↑/↓ меняют порядок категорий,
   «+» открывает выбор книг ИЗ УЖЕ СУЩЕСТВУЮЩИХ в списке, удаление убирает
   категорию (книги остаются в списке без категории). Заголовок — приёмник
   перетаскивания: бросок книги назначает её в эту категорию. */
function BookCatHeader({ listId, cat, count, adding, onToggleAdd, dragId, overId, setOverId, clearDrag }) {
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(cat.name);
  const save = () => { A.renameBookCat(listId, cat.id, draft); setEditing(false); };
  return (
    <div
      className={"book-header" + (overId === cat.id && dragId ? " book-header--over" : "")}
      onDragOver={(e) => { if (dragId) { e.preventDefault(); if (overId !== cat.id) setOverId(cat.id); } }}
      onDragLeave={() => { if (overId === cat.id) setOverId(null); }}
      onDrop={(e) => { e.preventDefault(); if (dragId) A.setBookItemCat(listId, dragId, cat.id); clearDrag(); }}>
      <span className="book-header__mark">▣</span>
      {editing ? (
        <input className="book-row__edit" autoFocus value={draft} onChange={(e) => setDraft(e.target.value)}
          onKeyDown={(e) => { if (e.key === "Enter") save(); if (e.key === "Escape") setEditing(false); }} onBlur={save} />
      ) : (
        <span className="book-header__title" onDoubleClick={() => { setDraft(cat.name); setEditing(true); }} title="Двойной клик — переименовать">{cat.name}</span>
      )}
      <span className="book-header__count">{count} кн.</span>
      <button className={"book-header__add" + (adding ? " book-header__add--on" : "")} onClick={onToggleAdd} title="Добавить книги из списка в эту категорию"><Icon name="plus" size={14} /></button>
      <button className="book-row__btn" onClick={() => A.moveBookCat(listId, cat.id, -1)} title="Категорию выше">↑</button>
      <button className="book-row__btn" onClick={() => A.moveBookCat(listId, cat.id, 1)} title="Категорию ниже">↓</button>
      <button className="book-row__btn book-row__del" onClick={() => { if (confirm("Удалить категорию «" + cat.name + "»? Книги останутся в списке.")) A.removeBookCat(listId, cat.id); }} title="Удалить категорию"><Icon name="close" size={13} /></button>
    </div>
  );
}

/* Выбор книг ИЗ УЖЕ СУЩЕСТВУЮЩИХ в списке для добавления в категорию.
   Галочка = книга в этой категории; клик переключает принадлежность. */
function CategoryMemberPicker({ listId, catId, books }) {
  const [q, setQ] = useState("");
  const lq = q.trim().toLowerCase();
  const list = (lq ? books.filter((b) => (b.text || "").toLowerCase().includes(lq)) : books);
  return (
    <div className="catpicker">
      <input autoFocus value={q} onChange={(e) => setQ(e.target.value)} placeholder="Поиск книги в списке…" className="catpicker__input" />
      <div className="catpicker__list">
        {list.length === 0 && <div className="muted" style={{ fontSize: "var(--t-small)", padding: "4px 2px" }}>Нет книг.</div>}
        {list.map((b) => {
          const on = b.cat === catId;
          return (
            <div key={b.id} className="qbook-opt" onClick={() => A.setBookItemCat(listId, b.id, on ? null : catId)}>
              <span className={"qbook-opt__box" + (on ? " qbook-opt__box--on" : "")}>{on && <Icon name="check" size={11} style={{ color: "#fff" }} />}</span>
              <span className="qbook-opt__title">{b.text}</span>
              {b.cat && b.cat !== catId && <span className="catpicker__other">в другой категории</span>}
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* Поле добавления НОВОЙ книги в список (поиск по библиотеке / свой текст). */
function AddBookBar({ listId, state }) {
  const Cmp = window;
  const [q, setQ] = useState("");
  const [openList, setOpenList] = useState(false);
  const mats = (state && state.materials) || [];
  const lq = q.trim().toLowerCase();
  const results = lq ? mats.filter((m) => ((m.title || "") + " " + (m.author || "")).toLowerCase().includes(lq)).slice(0, 30) : [];
  const addCustom = () => { if (q.trim()) { A.addBookItem(listId, q.trim()); setQ(""); setOpenList(false); } };
  const addLinked = (m) => { A.addBookItem(listId, m.title, m.id); setQ(""); setOpenList(false); };
  return (
    <div className="addbook" style={{ marginBottom: 12 }}>
      <div className="row-3" style={{ gap: 8 }}>
        <input value={q} onChange={(e) => { setQ(e.target.value); setOpenList(true); }} onFocus={() => setOpenList(true)}
          onKeyDown={(e) => { if (e.key === "Enter") addCustom(); if (e.key === "Escape") setOpenList(false); }}
          placeholder="Добавить новую книгу в список: поиск по библиотеке или свой текст…"
          style={{ flex: 1, border: "1px solid var(--ink-40)", background: "var(--paper-00)", padding: "8px 10px", fontFamily: "var(--font-slab)", fontSize: "var(--t-body)", outline: "none" }} />
        <Cmp.GButton variant="ink" icon="plus" onClick={addCustom}>Добавить</Cmp.GButton>
      </div>
      {openList && lq && (
        <div className="addbook__drop">
          <div className="addbook__custom" onClick={addCustom}><Icon name="plus" size={12} /> добавить «{q.trim()}» как свой пункт</div>
          {results.length > 0 && <div className="addbook__hint">из библиотеки — выберите, чтобы сразу привязать:</div>}
          {results.map((m) => (
            <div key={m.id} className="qbook-opt" onClick={() => addLinked(m)}>
              <Icon name={m.type === "video" ? "film" : "book"} size={13} />
              <span className="qbook-opt__title">{m.title}</span>
              {m.author && <span className="qbook-opt__author">{m.author}</span>}
            </div>
          ))}
          {results.length === 0 && <div className="muted" style={{ fontSize: "var(--t-small)", padding: "4px 2px" }}>В библиотеке не найдено — можно добавить как свой пункт.</div>}
        </div>
      )}
    </div>
  );
}

function BookQuestScreen(props) {
  const Cmp = window;
  const state = window.useStore();
  const lists = state.bookLists;
  const [dragId, setDragId] = useState(null);
  const [overId, setOverId] = useState(null);
  const [open, setOpen] = useState({});
  const [activeId, setActiveId] = useState(null);
  const [adding, setAdding] = useState(false);
  const [newListName, setNewListName] = useState("");
  const [renaming, setRenaming] = useState(false);
  const [renameDraft, setRenameDraft] = useState("");
  const [addingCat, setAddingCat] = useState(false);
  const [catName, setCatName] = useState("");
  const [adder, setAdder] = useState(null); /* id категории, под которой открыт выбор книг */
  const toggleOpen = (id) => setOpen((o) => ({ ...o, [id]: !o[id] }));
  const toggleAdder = (id) => setAdder((a) => (a === id ? null : id));
  const clearDrag = () => { setDragId(null); setOverId(null); };

  /* миграция в bookLists — только ПОСЛЕ загрузки состояния с сервера. */
  useEffect(() => {
    if (state.bookLists != null || !window.PlannerStateLoaded) return;
    const src = (Array.isArray(state.bookList) && state.bookList.length) ? state.bookList : window.DEFAULT_BOOKLIST;
    A.initBookLists([{ name: "Основной список", items: src }]);
  }, [state.bookLists, state.bookList]);

  /* миграция старых заголовков-строк в явные категории (L.cats + b.cat) */
  useEffect(() => {
    if (!window.PlannerStateLoaded || !Array.isArray(state.bookLists)) return;
    if (state.bookLists.some((L) => !Array.isArray(L.cats))) A.ensureListCats();
  }, [state.bookLists]);

  /* миграция старого формата глав (массив чисел) в объекты {id, mark, …} */
  useEffect(() => {
    if (!window.PlannerStateLoaded || !Array.isArray(state.bookLists)) return;
    const legacy = state.bookLists.some((L) => (L.items || []).some((b) => Array.isArray(b.chapters) && b.chapters.some((c) => typeof c === "number" || (c && !c.id))));
    if (legacy) A.normalizeBookChapters();
  }, [state.bookLists]);

  if (lists == null) return <div className="empty" style={{ padding: "var(--s-7)" }}>Загружаю списки книг…</div>;
  if (lists.length === 0) return <div className="empty" style={{ padding: "var(--s-7)" }}>Нет списков.</div>;

  /* по умолчанию — первый (оригинальный) список */
  const active = lists.find((L) => L.id === activeId) || lists[0];
  const listId = active.id;
  const items = (active.items || []).filter((x) => x.type !== "header"); /* книги */
  const cats = active.cats || [];
  const books = items;
  const counts = books.reduce((a, b) => { a[b.mark || 0] = (a[b.mark || 0] || 0) + 1; return a; }, {});
  const catIds = new Set(cats.map((c) => c.id));
  const booksOf = (catId) => items.filter((b) => b.cat === catId);
  const uncategorized = items.filter((b) => !b.cat || !catIds.has(b.cat));
  const createList = () => {
    const nm = newListName.trim();
    if (nm) { const id = A.addBookList(nm); setActiveId(id); }
    setNewListName(""); setAdding(false);
  };
  const saveRename = () => { if (renameDraft.trim()) A.renameBookList(listId, renameDraft.trim()); setRenaming(false); };
  const createCat = () => { if (catName.trim()) A.addBookCat(listId, catName.trim()); setCatName(""); setAddingCat(false); };
  let _bn = 0; /* сквозная нумерация книг */

  return (
    <div className="task" style={{ display: "block", padding: "var(--s-5)" }}>
      <Cmp.SectionRule kicker="Квест">Книги</Cmp.SectionRule>

      {/* переключатель списков (категорий) */}
      <div className="booklists-tabs">
        {lists.map((L) => (
          <button key={L.id} className={"booklists-tab" + (L.id === listId ? " booklists-tab--on" : "")}
            onClick={() => { setActiveId(L.id); setRenaming(false); }}>{L.name}</button>
        ))}
        {adding ? (
          <span className="booklists-add">
            <input autoFocus value={newListName} onChange={(e) => setNewListName(e.target.value)}
              onKeyDown={(e) => { if (e.key === "Enter") createList(); if (e.key === "Escape") { setAdding(false); setNewListName(""); } }}
              placeholder="Название списка…" className="booklists-add__input" />
            <button className="book-chapters__set" onClick={createList}>создать</button>
          </span>
        ) : (
          <button className="booklists-tab booklists-tab--add" onClick={() => setAdding(true)}>+ список</button>
        )}
      </div>

      <div className="row-3 wrap gap-4" style={{ margin: "var(--s-4) 0", alignItems: "center" }}>
        {renaming ? (
          <span className="booklists-add">
            <input autoFocus value={renameDraft} onChange={(e) => setRenameDraft(e.target.value)}
              onKeyDown={(e) => { if (e.key === "Enter") saveRename(); if (e.key === "Escape") setRenaming(false); }} onBlur={saveRename}
              className="booklists-add__input" />
          </span>
        ) : (
          <span className="e-label" style={{ fontSize: "var(--t-body)", color: "var(--text-primary)" }}>{active.name}</span>
        )}
        <span className="task__link" style={{ fontSize: "var(--t-small)" }} onClick={() => { setRenameDraft(active.name); setRenaming(true); }}><Icon name="edit" size={12} /> переименовать</span>
        {lists.length > 1 && <span className="task__link" style={{ fontSize: "var(--t-small)", color: "var(--signal)" }}
          onClick={() => { if (confirm("Удалить список «" + active.name + "» со всеми книгами?")) { A.removeBookList(listId); setActiveId(null); } }}><Icon name="close" size={12} /> удалить список</span>}
      </div>

      <div className="row-3 wrap gap-4" style={{ margin: "var(--s-3) 0", alignItems: "center" }}>
        <span className="e-label">всего {books.length}</span>
        <span className="booklist-stat booklist-stat--yellow">в процессе {counts[1] || 0}</span>
        <span className="booklist-stat booklist-stat--green">прочитано {counts[2] || 0}</span>
        {addingCat ? (
          <span className="booklists-add">
            <input autoFocus value={catName} onChange={(e) => setCatName(e.target.value)}
              onKeyDown={(e) => { if (e.key === "Enter") createCat(); if (e.key === "Escape") { setAddingCat(false); setCatName(""); } }}
              placeholder="Название категории…" className="booklists-add__input" />
            <button className="book-chapters__set" onClick={createCat}>добавить</button>
          </span>
        ) : (
          <span className="task__link" style={{ fontSize: "var(--t-small)" }} onClick={() => setAddingCat(true)}><Icon name="plus" size={12} /> категория</span>
        )}
      </div>
      <AddBookBar listId={listId} state={state} />
      <div className="booklist">
        {cats.map((cat) => (
          <React.Fragment key={cat.id}>
            <BookCatHeader listId={listId} cat={cat} count={booksOf(cat.id).length}
              adding={adder === cat.id} onToggleAdd={() => toggleAdder(cat.id)}
              dragId={dragId} overId={overId} setOverId={setOverId} clearDrag={clearDrag} />
            {adder === cat.id && (
              <div className="book-section-add">
                <CategoryMemberPicker listId={listId} catId={cat.id} books={items} />
              </div>
            )}
            {booksOf(cat.id).map((it) => (
              <BookRow key={it.id} listId={listId} it={it} idx={_bn++} open={!!open[it.id]} onToggle={toggleOpen}
                state={state} nav={props.nav} onUnassign={() => A.setBookItemCat(listId, it.id, null)}
                dragId={dragId} overId={overId} setDragId={setDragId} setOverId={setOverId} />
            ))}
          </React.Fragment>
        ))}
        {uncategorized.length > 0 && cats.length > 0 && <div className="book-uncat-label">Без категории</div>}
        {uncategorized.map((it) => (
          <BookRow key={it.id} listId={listId} it={it} idx={_bn++} open={!!open[it.id]} onToggle={toggleOpen}
            state={state} nav={props.nav}
            dragId={dragId} overId={overId} setDragId={setDragId} setOverId={setOverId} />
        ))}
      </div>
      {items.length === 0 && <div className="muted" style={{ fontSize: "var(--t-small)" }}>Список пуст. Добавьте книги выше.</div>}
    </div>
  );
}

window.Screens = Object.assign(window.Screens || {}, { questPage: CustomQuestPage, questCreate: CustomQuestCreate, questBooks: BookQuestScreen });
