// 표현 사전 — 본문·표현·어휘·저장함 어디서든 텍스트를 드래그하면 🔎 버튼이 떠서,
// 그 자리에서 뜻·예문·쓰임을 본다. 영어(Wiktionary)와 일본어(Jisho)가 같은 화면을 쓴다.
//
// 사전 사이트는 iframe으로 못 띄운다(X-Frame-Options) — 서버(server/lookup.js)가 공개 API에서
// 데이터만 받아오고, 여기서는 그걸 앱 디자인으로 그린다. 외부 사전 링크는 새 탭으로만 연다.
//
// classic <script type="text/babel" src="pages/Lookup.jsx"> 로 로드된다 — ES 모듈이 아니므로
// import/export 금지, 전역 함수 선언만 쓴다 (pages/RolePlay.jsx와 같은 규칙).
const { useState, useEffect, useRef, useCallback } = React;

const LOOKUP_MAX_LEN = 80;   // 이보다 길게 끌면 "사전 찾기"가 아니라 그냥 본문 복사다

// 드래그(또는 길게 눌러 선택)한 텍스트와 그 위치를 따라다닌다.
// selectionchange는 손가락이 움직이는 내내 쏟아지므로 한 박자 늦춰서 받는다.
function useTextSelection(enabled = true) {
  const [sel, setSel] = useState(null);   // { text, x, bottom, context }
  const timer = useRef(null);

  const clear = useCallback(() => setSel(null), []);

  useEffect(() => {
    if (!enabled) { setSel(null); return; }

    const read = () => {
      const s = window.getSelection?.();
      const text = s && !s.isCollapsed ? s.toString().trim() : '';
      if (!text || text.length > LOOKUP_MAX_LEN || !s.rangeCount) return setSel(null);

      const range = s.getRangeAt(0);
      const node  = range.commonAncestorContainer;
      const el    = node.nodeType === 1 ? node : node.parentElement;
      // 입력창·시트 안에서의 선택은 대상이 아니다 (시트 위에서 또 뜨면 겹친다)
      if (!el || el.closest('input, textarea, .pick-back, .lk-back, .audio-player')) return setSel(null);

      const r = range.getBoundingClientRect();
      if (!r || (!r.width && !r.height)) return setSel(null);

      // 뜻풀이에 문맥을 같이 주면 GPT 설명이 훨씬 정확해진다 — 그 표현이 있던 문장/카드
      const host = el.closest('.srow, .entry, .kb-card, .exbox, .script');
      setSel({
        text,
        x: Math.min(Math.max(r.left + r.width / 2, 70), window.innerWidth - 70),
        bottom: r.bottom,
        top: r.top,
        context: host?.innerText?.replace(/\s+/g, ' ').trim().slice(0, 300) ?? null,
      });
    };

    const onChange = () => {
      clearTimeout(timer.current);
      timer.current = setTimeout(read, 180);
    };
    document.addEventListener('selectionchange', onChange);
    return () => { document.removeEventListener('selectionchange', onChange); clearTimeout(timer.current); };
  }, [enabled]);

  return { sel, clear };
}

// 선택 영역 아래(공간이 없으면 위)에 뜨는 작은 버튼.
// onMouseDown에서 preventDefault를 해야 버튼을 누르는 순간 선택이 풀려 버튼이 사라지지 않는다.
function LookupFab({ sel, cls, onOpen }) {
  if (!sel) return null;
  const below = sel.bottom + 46 < window.innerHeight;
  return (
    <button
      className={`lookup-fab ${cls}`}
      style={{ left: `${sel.x}px`, top: `${below ? sel.bottom + 8 : sel.top - 40}px` }}
      onMouseDown={e => e.preventDefault()}
      onTouchStart={e => e.preventDefault()}
      onClick={() => onOpen(sel)}
    >
      🔎 사전
    </button>
  );
}

// ── 사전 시트 ────────────────────────────────────────────────
// 사전 뜻은 바로 뜨고(수십 ms), 실제 예문(Tatoeba)은 15~20초씩 걸려 뒤이어 채워진다.
function LookupSheet({ q, lang, context, cls, onClose }) {
  const [dict, setDict] = useState({ loading: true, data: null, error: null });
  const [ex, setEx]     = useState({ loading: true, items: [] });
  const [gpt, setGpt]   = useState({ loading: false, data: null, error: null });
  const openedAt = useRef(Date.now());
  const settled = () => Date.now() - openedAt.current > 250;

  useEffect(() => {
    let alive = true;
    setDict({ loading: true, data: null, error: null });
    setEx({ loading: true, items: [] });
    setGpt({ loading: false, data: null, error: null });

    apiFetch(`/api/lookup?q=${encodeURIComponent(q)}&lang=${lang}`)
      .then(d => { if (alive) setDict({ loading: false, data: d, error: null }); })
      .catch(e => { if (alive) setDict({ loading: false, data: null, error: e.message }); });

    apiFetch(`/api/lookup/examples?q=${encodeURIComponent(q)}&lang=${lang}`)
      .then(d => { if (alive) setEx({ loading: false, items: d.examples ?? [] }); })
      .catch(() => { if (alive) setEx({ loading: false, items: [] }); });

    return () => { alive = false; };
  }, [q, lang]);

  useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);

  const explain = async () => {
    setGpt({ loading: true, data: null, error: null });
    try {
      const d = await apiFetch('/api/lookup/explain', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ q, lang, context }),
      });
      setGpt({ loading: false, data: d, error: null });
    } catch (e) {
      setGpt({ loading: false, data: null, error: '설명을 받지 못했습니다' });
    }
  };

  const d = dict.data;
  const empty = !dict.loading && !dict.error && !d?.entries?.length;

  return (
    <div className="pick-back lk-back" onClick={() => { if (settled()) onClose(); }}>
      <div className={`pick ${cls}`} onClick={e => e.stopPropagation()}
           role="dialog" aria-modal="true" aria-label="사전">
        <div className="pick-hd">
          <span className="pick-title">🔎 사전</span>
          <button className="pick-x" onClick={onClose} aria-label="닫기">✕</button>
        </div>

        <div className="pick-body">
          <div className="lk-head">
            <div className="lk-word">{d?.headword || q}</div>
            {!!d?.readings?.length && (
              <div className="lk-read">
                {d.readings.map((r, i) => (
                  <span key={i}>{r.word ? `${r.word}（${r.reading}）` : r.reading}</span>
                ))}
              </div>
            )}
            {!!d?.tags?.length && (
              <div className="lk-tags">{d.tags.map((t, i) => <span key={i} className="lk-tag">{t}</span>)}</div>
            )}
          </div>

          {dict.loading && <div className="lk-note">사전을 찾는 중...</div>}
          {dict.error && <div className="lk-note">사전 조회 실패 — {dict.error}</div>}
          {empty && (
            <div className="lk-note">
              사전에 없는 표현입니다. 교재 특유의 구어체나 변형된 관용구면 아래 <b>쓰임 설명</b>이 더 정확합니다.
            </div>
          )}

          {(d?.entries ?? []).map((e, i) => (
            <div key={i} className="lk-sense">
              {e.pos && <div className="lk-pos">{e.pos}</div>}
              {e.defs.map((df, j) => (
                <div key={j} className="lk-def">
                  <span className="lk-num">{j + 1}</span>
                  <div>
                    <div className="lk-deft">{df.text}</div>
                    {df.examples.map((x, k) => <div key={k} className="lk-ex">{x}</div>)}
                  </div>
                </div>
              ))}
            </div>
          ))}

          <div className="lk-sec">
            <div className="lk-sec-t">실제 예문</div>
            {ex.loading && <div className="lk-note lk-slow">예문을 찾는 중... (Tatoeba는 15초쯤 걸립니다)</div>}
            {/* Tatoeba 검색은 어간까지 잡아 와서(hopefully → hope 문장들) 걸러내고 나면 비는 일이
                꽤 있다. 빈 칸만 두지 말고 아래 ✨ 버튼으로 안내한다. */}
            {!ex.loading && !ex.items.length && (
              <div className="lk-note">
                이 표현이 그대로 들어간 예문을 찾지 못했습니다 — 아래 <b>쓰임 설명</b>에서 예문을 만들 수 있습니다.
              </div>
            )}
            {ex.items.map((x, i) => (
              <div key={i} className="lk-sent">
                <div className="lk-sent-t">{x.text}</div>
                {x.ko && <div className="lk-sent-ko">{x.ko}</div>}
              </div>
            ))}
          </div>

          <div className="lk-sec">
            <div className="lk-sec-t">쓰임 설명 {lang === 'ja' && <span className="lk-hint">(일본어 사전 뜻은 영어입니다)</span>}</div>
            {!gpt.data && (
              <button className={`btn ${cls} ${gpt.loading ? 'generating' : ''}`} onClick={explain} disabled={gpt.loading}>
                {gpt.loading ? '설명 받는 중...' : '✨ 한국어로 쓰임 보기'}
              </button>
            )}
            {gpt.error && <div className="lk-note">{gpt.error}</div>}
            {gpt.data && (
              <>
                {gpt.data.meaning_ko && <div className="lk-mean">{gpt.data.meaning_ko}</div>}
                {gpt.data.usage_ko && <div className="lk-usage">{gpt.data.usage_ko}</div>}
                {(gpt.data.examples ?? []).map((x, i) => (
                  <div key={i} className="lk-sent">
                    <div className="lk-sent-t">{x.text}</div>
                    {x.ko && <div className="lk-sent-ko">{x.ko}</div>}
                  </div>
                ))}
              </>
            )}
          </div>
        </div>

        <div className="pick-ft">
          <div className="lk-links">
            {(d?.links ?? []).map((l, i) => (
              <a key={i} className="btn" href={l.url} target="_blank" rel="noopener noreferrer">{l.label} ↗</a>
            ))}
            <span style={{ flex: 1 }} />
            <button className="btn" onClick={onClose}>닫기</button>
          </div>
        </div>
      </div>
    </div>
  );
}

// 선택 → 🔎 → 시트까지 한 벌로 묶은 것. App/JapaneseApp이 이것만 렌더하면 된다.
function LookupLayer({ lang, cls }) {
  const { sel, clear } = useTextSelection(true);
  const [open, setOpen] = useState(null);   // { q, context }

  return (
    <>
      <LookupStyles />
      {!open && <LookupFab sel={sel} cls={cls} onOpen={s => { setOpen({ q: s.text, context: s.context }); clear(); }} />}
      {open && (
        <LookupSheet
          q={open.q} lang={lang} context={open.context} cls={cls}
          onClose={() => { setOpen(null); window.getSelection?.()?.removeAllRanges?.(); }}
        />
      )}
    </>
  );
}

function LookupStyles() {
  return <style>{LOOKUP_CSS}</style>;
}

const LOOKUP_CSS = `
.lookup-fab{
  position:fixed;z-index:390;transform:translateX(-50%);
  padding:7px 13px;border-radius:var(--pill);border:1px solid var(--ac);
  background:var(--ac);color:#fff;font-size:12.5px;font-weight:700;white-space:nowrap;
  box-shadow:var(--sh-pop);animation:lk-pop .16s var(--ease);
}
.lookup-fab:hover{background:var(--ac-dark);border-color:var(--ac-dark)}
@keyframes lk-pop{from{opacity:0;transform:translateX(-50%) scale(.9)}}

.lk-head{padding-bottom:12px;border-bottom:1px solid var(--border);margin-bottom:13px}
.lk-word{font-family:var(--font-serif);font-size:24px;font-weight:600;line-height:1.2;color:var(--text);word-break:break-word}
.lk-read{margin-top:5px;font-size:13.5px;color:var(--muted);display:flex;gap:10px;flex-wrap:wrap}
.lk-tags{margin-top:8px;display:flex;gap:5px;flex-wrap:wrap}
.lk-tag{font-size:10px;font-weight:700;letter-spacing:.04em;color:var(--ac-ink);background:var(--ac-soft);
  border:1px solid var(--ac-line);border-radius:var(--r-xs);padding:2px 7px}

.lk-sense + .lk-sense{margin-top:14px;padding-top:12px;border-top:1px dashed var(--border)}
.lk-pos{font-size:10.5px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--ac-ink);margin-bottom:7px}
.lk-def{display:flex;gap:9px;margin-bottom:9px}
.lk-num{flex-shrink:0;width:17px;height:17px;margin-top:2px;display:grid;place-items:center;border-radius:50%;
  background:var(--surface3);color:var(--muted);font-size:10px;font-weight:700}
.lk-deft{font-size:14.5px;line-height:1.55;color:var(--text)}
.lk-ex{margin-top:5px;padding-left:10px;border-left:2px solid var(--ac-line);font-size:12.5px;color:var(--muted);line-height:1.5}

.lk-sec{margin-top:16px;padding-top:13px;border-top:1px solid var(--border)}
.lk-sec-t{font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--muted2);margin-bottom:9px}
.lk-hint{font-weight:600;letter-spacing:0;text-transform:none;opacity:.8}
.lk-sent{background:var(--surface2);border-radius:var(--r-sm);padding:9px 12px;margin-bottom:7px}
.lk-sent-t{font-size:14px;font-weight:600;line-height:1.5;color:var(--text)}
.lk-sent-ko{font-size:12.5px;color:var(--muted);margin-top:3px;line-height:1.5}
.lk-mean{font-size:15px;font-weight:700;color:var(--ac-ink);margin-bottom:6px}
.lk-usage{font-size:13.5px;line-height:1.7;color:var(--text);margin-bottom:10px}
.lk-note{font-size:12.5px;color:var(--muted2);line-height:1.6;padding:2px 0 4px}
.lk-slow{animation:pulse 1.6s ease-in-out infinite}
.lk-links{display:flex;gap:7px;align-items:center;flex-wrap:wrap}
.lk-links .btn{text-decoration:none}
`;
