// Sift.jsx — 筛词阅读 v2（第十八轮）：左文右词的页内工作台（不弹窗）
// 级别：小学/初中/高中 + Me词（我的生词本视图）。筛词与释义都由服务端 LLM 批量完成。
const { useState: useStateSF, useEffect: useEffectSF, useMemo: useMemoSF } = React;
const SIFT_LEVELS = ['小学', '初中', '高中', '四级', '六级', '托福'];

function SiftReader({ text, title, noteId, onAdopt, onClose }) {
  const [fixed, setFixed] = useStateSF(null);   // §23 固化命中：直接用，零 API
  const [examOpen, setExamOpen] = useStateSF(false);   // 同卷模考（§25 执行器）
  const pages = useMemoSF(() => {
    const paras = String(text || '').split('\n').filter(x => x.trim());
    const out = []; let buf = []; let n = 0;
    paras.forEach(p => { buf.push(p); n += p.length; if (n > 1300) { out.push(buf); buf = []; n = 0; } });
    if (buf.length) out.push(buf);
    return out.length ? out : [['（这一页还没有正文）']];
  }, [text]);
  const [level, setLevel] = useStateSF('小学');   // §34 默认小学；红线只画当前级别的筛词结果（换级别红线跟着换）
  const [done, setDone] = useStateSF(0);        // 已筛到第几段（连续滚动，后台逐段筛）
  const realId = (typeof window.poporRealId === 'function' && noteId) ? window.poporRealId(noteId) : noteId;
  const [meView, setMeView] = useStateSF(false);          // Me词：我的生词本视图
  const [cache, setCache] = useStateSF({});               // {pg|level: [{w,d}]}
  const [busy, setBusy] = useStateSF(false);
  const [bag, setBag] = useStateSF([]);                   // 收下的词（服务器 vocab.json，追加式义项）
  useEffectSF(() => {
    fetch((window.POPOR_API_BASE || '') + '/api/vocab').then(r => r.json())
      .then(x => x && setBag((x.items || []).map(e => ({ w: e.w, d: e.d })))).catch(() => {});
  }, []);
  const [flash, setFlash] = useStateSF('');
  // 合并所有已筛段的词（去重，保留先出现的释义）
  const entries = useMemoSF(() => {
    const low = String(text || '').toLowerCase();
    const srt = l => l.slice().sort((a, b) => {
      const ia = low.indexOf(String(a.w || '').toLowerCase());
      const ib = low.indexOf(String(b.w || '').toLowerCase());
      return (ia < 0 ? 1e9 : ia) - (ib < 0 ? 1e9 : ib);
    });   // 铁律：词表顺序 = 原文顺序（不信模型，信 indexOf）
    if (fixed) return srt(fixed);
    const seen = {}; const out = [];
    for (let i = 0; i < pages.length; i++) {
      (cache[i + '|' + level] || []).forEach(e => {
        const w = String(e.w).toLowerCase();
        if (!seen[w]) { seen[w] = 1; out.push({ w, d: e.d }); }
      });
    }
    return srt(out);
  }, [cache, level, pages, fixed, text]);

  async function llm(prompt, body) {
    const r = await fetch((window.POPOR_API_BASE || '') + '/api/skill/text', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ prompt, text: body }),
    });
    const d = await r.json().catch(() => ({}));
    if (!d || d.status !== 'ok') throw new Error('引擎未响应');
    return d.text;
  }
  useEffectSF(() => {
    let dead = false;
    setFixed(null);
    setDone(0); setBusy(true);
    (async () => {
      // 先查固化：这卷这级别筛过 = 直接用，不再走任何 API（§23）
      if (/^note_/.test(String(realId || ''))) {
        try {
          const cd = await fetch((window.POPOR_API_BASE || '') + '/api/notes/' + realId + '/sift?level=' + encodeURIComponent(level)).then(r => r.json());
          if (!dead && cd && cd.entries && cd.entries.length) { setFixed(cd.entries); setBusy(false); return; }
        } catch (e) {}
      }
      for (let i = 0; i < pages.length; i++) {
        if (dead) return;
        if (!cache[i + '|' + level]) {
          try {
            const t = await llm('从下面英文里筛出超过中国「' + level + '」词汇水平的**单词和短语**（动词短语、固定搭配、习语都要选，短语照原文小写；学过' + level + '就不选）。只输出 JSON 数组，严格按原文出现顺序，最多 26 个：[{"w":"单词小写原形","d":"词性·中文释义(≤14字)"}]', pages[i].join('\n'));
            let arr = [];
            try { arr = JSON.parse(t.slice(t.indexOf('['), t.lastIndexOf(']') + 1)); } catch (e) {}
            if (dead) return;
            let out = (arr || []).filter(x => x && x.w);
            // ConDict 优先：C 栏有语境释义就用词典的；AI 生成的回写喂库（Popor 读者养 ConDict）
            try {
              const dr = await fetch('https://condict.satest.com.cn/lookup_batch_v2', {
                method: 'POST', headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ words: out.map(x => x.w) }),
              }).then(r => r.json());
              const res = (dr && dr.results) || {};
              const wb = {};
              out = out.map(x => {
                const info = res[x.w] || res[String(x.w).toLowerCase()];
                // 修：C 栏个别释义是嵌套对象（老工具写入的结构格式），只收字符串，其余回落 AI 释义
                if (info && info.from_c && typeof info.meaning === 'string' && info.meaning) {
                  const pm = info.meaning.match(/^\{['"]m['"]:\s*['"]([^'"]*)['"]/);   // 洗 Python 字典字符串形态
                  const clean = pm ? pm[1] : info.meaning;
                  if (clean) return { w: x.w, d: clean };
                }
                wb[x.w] = x.d.replace(/^[^·]*·/, '');   // 回写去掉词性前缀，存干净释义
                return x;
              });
              if (Object.keys(wb).length) {
                fetch('https://condict.satest.com.cn/submit_meanings_batch', {
                  method: 'POST', headers: { 'Content-Type': 'application/json' },
                  body: JSON.stringify({ meanings: {}, word_fallback: wb }),
                }).catch(() => {});
              }
            } catch (e) { /* 词典不在线就全用 AI 的，静默回落 */ }
            setCache(c => Object.assign({}, c, { [i + '|' + level]: out }));
          } catch (e) {
            if (dead) return;
            setCache(c => Object.assign({}, c, { [i + '|' + level]: [] }));
          }
        }
        if (!dead) setDone(i + 1);
      }
      if (!dead) setBusy(false);
    })();
    return () => { dead = true; };
  }, [level]);
  // 全文筛完 → 自动固化（下一个人零成本）
  useEffectSF(() => {
    if (busy || fixed || !entries.length || !/^note_/.test(String(realId || ''))) return;
    if (done < pages.length) return;
    fetch((window.POPOR_API_BASE || '') + '/api/notes/' + realId + '/sift', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ level, entries }),
    }).catch(() => {});
  }, [busy, done]);

  const hardSet = useMemoSF(() => new Set(entries.map(e => String(e.w).toLowerCase())), [entries]);
  function hitWord(lw) { return hardSet.has(lw) || (lw.endsWith('s') && hardSet.has(lw.slice(0, -1))) || (lw.endsWith('ed') && hardSet.has(lw.slice(0, -2))); }
  function baseOf(lw) { if (hardSet.has(lw)) return lw; if (lw.endsWith('s') && hardSet.has(lw.slice(0, -1))) return lw.slice(0, -1); if (lw.endsWith('ed') && hardSet.has(lw.slice(0, -2))) return lw.slice(0, -2); return lw; }
  function toggle(e) {
    const has = bag.some(x => x.w === e.w && x.d === e.d);
    setBag(b => has ? b.filter(x => !(x.w === e.w && x.d === e.d)) : b.concat([{ w: e.w, d: e.d }]));
    fetch((window.POPOR_API_BASE || '') + '/api/vocab/' + (has ? 'remove' : 'add'), {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ w: e.w, d: e.d, src: title || '' }),
    }).catch(() => {});
  }
  function jumpList(w) {
    setFlash(w); setTimeout(() => setFlash(''), 1400);
    const el = document.getElementById('sfw-' + w);
    if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
  }
  // §38 短语划线：词表里的多词条目（动词短语/习语）此前从未被划中——渲染只按单词切。
  // 现在两层：先整段按短语（大小写不敏感、词间任意空白）切出命中，剩余部分再按单词划。
  const phraseSet = useMemoSF(() => new Set(entries.map(e => String(e.w).toLowerCase()).filter(w => w.indexOf(' ') >= 0)), [entries]);
  const phraseRe = useMemoSF(() => {
    const arr = Array.from(phraseSet);
    if (!arr.length) return null;
    const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '\\s+');
    return new RegExp('(' + arr.sort((a, b) => b.length - a.length).map(esc).join('|') + ')', 'gi');
  }, [phraseSet]);
  function renderWords(tx, kp) {
    const parts = String(tx).split(/(\b[A-Za-z][A-Za-z'-]*\b)/g);
    return parts.map((w, i) => {
      const lw = w.toLowerCase();
      if (!/^[a-z]/.test(lw) || !hitWord(lw)) return w;
      const base = baseOf(lw);
      return <em key={kp + '-' + i} className={'sf-hard' + (bag.some(b => b.w === base) ? ' got' : '')} onClick={() => jumpList(base)}>{w}</em>;
    });
  }
  function renderPara(tx, pi) {
    const chunks = phraseRe ? String(tx).split(phraseRe) : [tx];
    return (
      <p key={pi} className="sf2-para">
        {chunks.map((ck, ci) => {
          const norm = String(ck || '').toLowerCase().replace(/\s+/g, ' ').trim();
          if (phraseRe && phraseSet.has(norm)) {
            return <em key={'ph' + ci} className={'sf-hard' + (bag.some(b => b.w === norm) ? ' got' : '')} onClick={() => jumpList(norm)}>{ck}</em>;
          }
          return <React.Fragment key={'ck' + ci}>{renderWords(ck, ci)}</React.Fragment>;
        })}
      </p>
    );
  }
  const sideList = meView ? bag : entries;   // Me词=服务器生词本；同词多义项各自成行（义项即条目）
  return (
    <div className="sf2">
      <div className="sf2-head">
        <b>筛词阅读</b><span className="sf-sub">{title || '英文文本'}</span>
        <div className="illo-styles" style={{ marginLeft: 'auto' }}>
          {SIFT_LEVELS.map(l => <button key={l} className={'illo-style' + (!meView && level === l ? ' on' : '')} onClick={() => { setMeView(false); setLevel(l); }}>{l}</button>)}
          <button className={'illo-style' + (meView ? ' on' : '')} onClick={() => setMeView(true)}>Me 词 · {bag.length}</button>
          <button className="illo-style" title="用这套卷直接开考（AI 判分）" onClick={() => {
            if (!/^note_/.test(String(realId || ''))) { alert('先「存入时空轴」，再对这套卷开模考。'); return; }
            setExamOpen(true);
          }}>模考 ›</button>
          {examOpen && window.ExamRunner ? React.createElement(window.ExamRunner, { noteId: realId, onClose: () => setExamOpen(false) }) : null}
        </div>
        <button className="pins-del" onClick={onClose}><Icon name="x" size={14} /></button>
      </div>
      <div className="sf2-main">
        <div className="sf2-text">
          {pages.map((chunk, ci) => chunk.map((tx, pi) => renderPara(tx, ci + '-' + pi)))}
        </div>
        <div className="sf2-side">
          <div className="sf2-side-h">{meView ? '我的生词本' : '生词 · ' + level + ' · ' + entries.length + (busy ? '（筛到 ' + done + '/' + pages.length + ' 段…）' : '')}</div>
          {busy && !meView && !entries.length ? <div className="ntf-empty">正在按「{level}」筛词…</div> : null}
          {!busy && sideList.length === 0 && <div className="ntf-empty">{meView ? '还没收词。点右栏 ○ 或正文里的词。' : '这页没有超出该级别的词。'}</div>}
          {sideList.map(e => (
            <div key={e.w} id={'sfw-' + e.w} className={'sf2-w' + (flash === e.w ? ' flash' : '')}>
              <button className={'sf2-star' + (bag.some(x => x.w === e.w) ? ' on' : '')} title="收进生词本" onClick={() => toggle(e)}>{bag.some(x => x.w === e.w) ? '●' : '○'}</button>
              <div><b>{e.w}</b><p>{typeof e.d === 'string' ? e.d : String(e.d && (e.d.m || e.d.meaning) || '')}</p></div>
            </div>
          ))}
        </div>
      </div>
      <div className="sf2-foot">
        <span className="sf-bagn" style={{ marginLeft: 0 }}>{fixed ? '已固化 · ' + entries.length + ' 个生词（零成本复用）' : busy ? '正在按「' + level + '」逐段筛词 · ' + done + '/' + pages.length : '全文筛完 · ' + entries.length + ' 个生词'}</span>
        <span className="sf-bagn">生词本 · {bag.length}</span>
        {bag.length > 0 && <button className="illo-go" onClick={() => { onAdopt('—— 生词 · ' + (title || '筛词阅读') + ' ——\n' + bag.map(b => b.w + ' — ' + b.d).join('\n')); onClose(); }}><Icon name="check" size={12} /> 把生词表写进这页末尾</button>}
      </div>
    </div>
  );
}
Object.assign(window, { SiftReader });
