/* ============================================================
   TopicDetail — a topical vocabulary category (Food, Family,
   Weather, Animals…) opened from the Categories TOPICS grid.

   2026-06-06 — Topics feature. Header (emblem, name, blurb), a
   Words | Phrases segmented toggle (counts in the labels), each
   tab a WordGrid-style list: tap-to-hear (speakRU; clips
   pre-seeded), a check-ring to mark learned, a SaveHeart, and
   row-expand into the per-token StackedGloss view.

   Progress persists with the alphabet.jsx dual-write pattern:
   anon → localStorage key `ru_topic_<id>` ({ '<ru>': 1 }); signed
   in → user_progress upsert/delete with item_type 'word'|'phrase'
   and item_key `topic:<id>:<ru>`.

   Data contract: window.APP.topics = [{ id, name, emblem, color,
   tint, blurb, words:[item], phrases:[item] }]. topics may be
   undefined/empty until the content pipeline ships — TopicDetail
   renders a graceful "coming soon" state in that case.
   Status: ⚠️ PENDING USER TESTING
   ============================================================ */
(function () {
  const { useState, useRef, useEffect } = React;

  function findTopic(id) {
    const topics = (window.APP && window.APP.topics) || [];
    return topics.find(t => t.id === id) || null;
  }

  // 2026-06-06 — Strip EDITORIAL/authoring metadata from learner-facing note
  // text. The topics dataset (and lesson teach[] data app-wide) carries
  // pipeline markers like "(pending native review)" and "(reuse: …)" baked
  // into `note`. These are authoring annotations, NOT learner content — when
  // rendered they read as "(PENDING NATIVE REVIEW)" clutter in every row and
  // in the StackedGloss type-label header. We remove the parenthetical at
  // RENDER time only (underlying window.APP data is untouched). Real learner
  // notes survive: register hints ('informal', 'formal/polite'), literal
  // glosses ('lit. "I want to eat"'), and sound-change cues ('б→p', 'ж→sh').
  function cleanNote(note) {
    if (!note) return note;
    let s = String(note)
      .replace(/\(\s*pending native review\s*\)/gi, '')
      .replace(/\(\s*reuse:[^)]*\)/gi, '')
      .replace(/\s{2,}/g, ' ')
      .replace(/\s+([·,;])/g, '$1')
      .replace(/[·,;\s]+$/g, '')
      .trim();
    return s.length ? s : null;
  }
  // Return a shallow copy of a StackGloss item with its note sanitized, so
  // the row, the expanded gloss, and any save snapshot all show clean text.
  function cleanItem(item) {
    if (!item) return item;
    const cleaned = cleanNote(item.note);
    if (cleaned === item.note) return item; // nothing to strip — avoid re-alloc
    return { ...item, note: cleaned };
  }

  // 2026-06-06 — Emoji emblem tile. The shared <Emblem> renders a single
  // white Cyrillic glyph (Golos Text, size*0.5) on a SATURATED color tile —
  // designed for letters like 'А'. Topic emblems are emoji (🍎 👪 …), which
  // ignore the white `color`, render small at size*0.5, and clash with the
  // saturated background + colored drop-shadow. This variant places the emoji
  // on the topic's light TINT (falling back to a derived wash), at ~size*0.52
  // with no color override and a soft neutral shadow — so emoji read as icons
  // and still color-code, matching the app's calm surface aesthetic.
  function TopicEmblem({ emoji, color, tint, size = 42, radius = 13 }) {
    return (
      <div style={{
        width:size, height:size, borderRadius:radius, flexShrink:0,
        background: tint || 'var(--chip)',
        display:'flex', alignItems:'center', justifyContent:'center',
        boxShadow: `inset 0 0 0 1px ${color}22`,
      }}>
        <span style={{ fontSize: Math.round(size * 0.52), lineHeight:1, transform:'translateY(0.5px)' }}>{emoji}</span>
      </div>
    );
  }
  window.TopicEmblem = TopicEmblem;

  function Seg({ options, value, onChange }) {
    return (
      <div style={{ display:'flex', gap:4, background:'var(--chip)', borderRadius:12, padding:4 }}>
        {options.map(o => (
          <button key={o.id} onClick={() => onChange(o.id)}
            style={{ appearance:'none', border:'none', cursor:'pointer', flex:1, borderRadius:9,
              padding:'9px 8px', fontFamily:'"Hanken Grotesk",system-ui', fontWeight:800, fontSize:14,
              background: value === o.id ? 'var(--surface)' : 'transparent',
              color: value === o.id ? 'var(--ink)' : 'var(--muted)',
              boxShadow: value === o.id ? '0 1px 4px -1px rgba(0,0,0,0.15)' : 'none', transition:'all .15s' }}>
            {o.label}
          </button>
        ))}
      </div>
    );
  }

  function ItemRow({ item: rawItem, kind, color, isOn, isActive, expanded, onPlay, onToggle, onExpand, surface }) {
    const item = cleanItem(rawItem); // strip editorial markers from displayed note
    return (
      /* 2026-06-06 12:25PM — flexShrink:0 on the card below is REQUIRED. This card is a
         child of a `display:flex; flex-direction:column; flex:1` scroll list (the
         TopicDetail item list). Without flexShrink:0, when total rows exceed the viewport
         (e.g. 16 words) flexbox shrinks every row to fit instead of scrolling, and the
         card's overflow:hidden then clips the 3-line StackRow to a ~25px sliver
         (Christopher's screenshots, 2026-06-06). ✅ VERIFIED 2026-06-06 12:40PM —
         deployed build measured: row 93px, list scrollable, all 3 lines fit;
         Christopher confirmed "that fixed the formatting". */
      <div style={{ background:'var(--surface)', borderRadius:16, flexShrink:0,
        border:`1.5px solid ${isActive ? 'var(--primary)' : (isOn ? 'oklch(0.80 0.11 150)' : 'var(--line)')}`,
        boxShadow: isActive ? '0 8px 22px -12px var(--primary)' : '0 1px 0 var(--line)',
        transition:'border-color .15s, box-shadow .15s', overflow:'hidden' }}>
        {/* collapsed row — tap to hear; chevron expands the StackedGloss */}
        <div onClick={onPlay} role="button" tabIndex={0}
          onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onPlay(); } }}
          style={{ cursor:'pointer', textAlign:'left', padding:'14px 15px',
            display:'flex', alignItems:'center', gap:12 }}>
          <div style={{ flex:1, minWidth:0 }}>
            <StackRow item={item} active={isActive} />
          </div>
          <div style={{ display:'flex', alignItems:'center', gap:8, flexShrink:0 }}>
            {window.SaveHeart && (
              <window.SaveHeart item={item}
                lemmaId={item.lemma_id != null ? item.lemma_id : null}
                kind={kind} surface={surface} size={19} />
            )}
            <span onClick={(e) => { e.stopPropagation(); onToggle(); }} role="checkbox" aria-checked={isOn}
              style={{ width:22, height:22, borderRadius:999, flexShrink:0, cursor:'pointer',
                display:'flex', alignItems:'center', justifyContent:'center',
                background: isOn ? 'oklch(0.60 0.13 150)' : 'transparent',
                border: isOn ? 'none' : '1.5px solid var(--line)', transition:'background .15s' }}>
              {isOn && <svg width="13" height="13" viewBox="0 0 24 24"><path d="M5 13l4 4 10-11" stroke="#fff" strokeWidth="3" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>}
            </span>
            <span onClick={(e) => { e.stopPropagation(); onExpand(); }} role="button" aria-label={expanded ? 'Collapse' : 'Break it down'}
              style={{ cursor:'pointer', display:'flex', padding:4 }}>
              <svg width="16" height="16" viewBox="0 0 24 24"
                style={{ transform: expanded ? 'rotate(180deg)' : 'none', transition:'transform .2s' }}>
                <path d="M5 9l7 7 7-7" stroke="var(--muted)" strokeWidth="2.4" fill="none" strokeLinecap="round" strokeLinejoin="round" />
              </svg>
            </span>
          </div>
        </div>
        {/* expanded per-token gloss */}
        {expanded && (
          <div style={{ padding:'2px 12px 14px', borderTop:'1px dashed var(--line)' }}>
            <div style={{ marginTop:10 }}>
              <StackedGloss item={item} surface={surface} saveKind={kind} compact />
            </div>
          </div>
        )}
      </div>
    );
  }

  function TopicDetail({ nav, topicId }) {
    const topic = findTopic(topicId);

    // hooks must run unconditionally (graceful-missing handled in render)
    const KEY = 'ru_topic_' + topicId;
    const [view, setView] = useState('words');
    const [learned, setLearned] = useState(() => {
      try { return JSON.parse(localStorage.getItem(KEY)) || {}; } catch (e) { return {}; }
    });
    const [active, setActive] = useState(null);
    const [expanded, setExpanded] = useState(null);
    const [userId, setUserId] = useState(null);
    const [hint, setHint] = useState(false);
    const tRef = useRef(null);
    useEffect(() => () => clearTimeout(tRef.current), []);

    const words = (topic && topic.words) || [];
    const phrases = (topic && topic.phrases) || [];

    // pre-warm clips on mount so taps are instant
    useEffect(() => {
      if (!topic) return;
      if (window.warmAudio) window.warmAudio([...words, ...phrases].map(it => it.say || it.ru));
    }, [topicId]); // eslint-disable-line

    // signed-in: source the learned set from user_progress (both item types for this topic)
    useEffect(() => {
      let live = true;
      (async () => {
        if (!window.sb || !topic) return;
        const { data } = await window.sb.auth.getUser();
        const id = data && data.user ? data.user.id : null;
        if (!live) return;
        setUserId(id);
        if (id) {
          const prefix = 'topic:' + topicId + ':';
          const { data: rows, error } = await window.sb.from('user_progress')
            .select('item_key').eq('user_id', id).in('item_type', ['word', 'phrase']).eq('status', 'learned')
            .like('item_key', prefix + '%');
          if (error) { console.error('[topics] read', error.message); return; }
          if (!live) return;
          const map = {};
          (rows || []).forEach(r => { const ru = r.item_key.slice(prefix.length); if (ru) map[ru] = 1; });
          setLearned(map);
        }
      })();
      return () => { live = false; };
    }, [topicId]); // eslint-disable-line

    const items = view === 'words' ? words : phrases;
    const itemKind = view === 'words' ? 'word' : 'phrase';

    const play = (it) => {
      setActive(it.ru);
      window.speakRU(it.say || it.ru);
      clearTimeout(tRef.current);
      tRef.current = setTimeout(() => setActive(null), 1100);
    };
    const toggle = (it, kind) => {
      const wasOn = !!learned[it.ru];
      setLearned(prev => {
        const n = { ...prev };
        if (n[it.ru]) delete n[it.ru]; else n[it.ru] = 1;
        try { localStorage.setItem(KEY, JSON.stringify(n)); } catch (er) {}
        return n;
      });
      if (userId && window.sb) {
        const item_key = 'topic:' + topicId + ':' + it.ru;
        if (wasOn) {
          window.sb.from('user_progress').delete()
            .eq('user_id', userId).eq('item_type', kind).eq('item_key', item_key)
            .then(({ error }) => { if (error) console.error('[topics] delete', error.message); });
        } else {
          window.sb.from('user_progress').upsert(
            { user_id: userId, item_type: kind, item_key, status: 'learned', completed_at: new Date().toISOString() },
            { onConflict: 'user_id,item_type,item_key' }
          ).then(({ error }) => { if (error) console.error('[topics] upsert', error.message); });
        }
      } else if (!wasOn) {
        setHint(true);
      }
    };

    // ---- graceful missing-topic state -----------------------------
    if (!topic) {
      return (
        <div style={{ height:'100%', display:'flex', flexDirection:'column', background:'var(--bg)' }}>
          <div style={{ padding:'50px 18px 0', display:'flex', justifyContent:'flex-end' }}>
            <IconBtn kind="close" onClick={nav.back} />
          </div>
          <div style={{ flex:1, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center',
            gap:10, padding:'0 30px', textAlign:'center' }}>
            <div style={{ fontFamily:'"Bricolage Grotesque",system-ui', fontWeight:800, fontSize:22, color:'var(--ink)' }}>
              Topic coming soon</div>
            <div style={{ fontFamily:'"Hanken Grotesk",system-ui', fontWeight:600, fontSize:13.5, color:'var(--muted)',
              maxWidth:260, lineHeight:1.4 }}>
              This collection isn’t ready yet — check back shortly.</div>
          </div>
        </div>
      );
    }

    const color = topic.color || 'var(--primary)';
    const total = items.length;
    const count = items.filter(it => learned[it.ru]).length;

    return (
      <div style={{ height:'100%', display:'flex', flexDirection:'column', background:'var(--bg)', position:'relative' }}>
        {/* header — mirrors WordGrid/AlphabetGrid: top bar (emblem + label + close),
            then a big Bricolage title + subtitle, then the toggle + progress. */}
        <div style={{ padding:'50px 18px 0', display:'flex', alignItems:'center', justifyContent:'space-between', gap:12 }}>
          <div style={{ display:'flex', alignItems:'center', gap:9, minWidth:0 }}>
            <TopicEmblem emoji={topic.emblem} color={color} tint={topic.tint} size={28} radius={9} />
            <span style={{ fontFamily:'"Hanken Grotesk",system-ui', fontWeight:700, fontSize:13.5,
              color:color, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>Topics</span>
          </div>
          <IconBtn kind="close" onClick={nav.back} />
        </div>
        <div style={{ padding:'10px 22px 12px' }}>
          <div style={{ fontFamily:'"Bricolage Grotesque",system-ui', fontWeight:800, fontSize:27,
            color:'var(--ink)', letterSpacing:'-0.02em' }}>{topic.name}</div>
          <div style={{ fontFamily:'"Hanken Grotesk",system-ui', fontWeight:600, fontSize:13,
            color:'var(--muted)', marginTop:2 }}>{topic.blurb || 'Tap a card to hear it · check each one you’ve learned'}</div>
          <div style={{ marginTop:12 }}>
          <Seg value={view} onChange={(v) => { setView(v); setActive(null); setExpanded(null); }}
            options={[
              { id:'words', label:`Words · ${words.length}` },
              { id:'phrases', label:`Phrases · ${phrases.length}` },
            ]} />
          </div>
          <div style={{ display:'flex', alignItems:'center', gap:12, marginTop:12 }}>
            <div style={{ flex:1 }}><Bar value={total ? count/total : 0} color={color} /></div>
            <span style={{ fontFamily:'"JetBrains Mono",monospace', fontWeight:700, fontSize:12.5,
              color:'var(--ink)', whiteSpace:'nowrap' }}>{count}<span style={{ color:'var(--muted)' }}>/{total} learned</span></span>
          </div>
        </div>

        {/* anonymous "sign in to save" hint */}
        {hint && !userId && (
          <div style={{ margin:'2px 18px 8px', display:'flex', alignItems:'center', gap:10,
            background:'var(--primary-wash)', border:'1px solid var(--line)', borderRadius:14, padding:'10px 12px' }}>
            <span style={{ flex:1, fontFamily:'"Hanken Grotesk",system-ui', fontWeight:700, fontSize:12.5, color:'var(--primary-deep)' }}>
              Sign in to save your progress</span>
            <button onClick={() => nav.go('auth')} style={{ appearance:'none', border:'none', cursor:'pointer',
              background:'var(--primary)', color:'#fff', borderRadius:999, padding:'6px 12px',
              fontFamily:'"Hanken Grotesk",system-ui', fontWeight:800, fontSize:12 }}>Sign in</button>
            <button onClick={() => setHint(false)} aria-label="Dismiss" style={{ appearance:'none', border:'none', cursor:'pointer',
              background:'transparent', color:'var(--muted)', padding:'4px', display:'flex' }}>
              <svg width="16" height="16" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" strokeWidth="2.2" fill="none" strokeLinecap="round"/></svg>
            </button>
          </div>
        )}

        {/* item list */}
        <div style={{ flex:1, minHeight:0, overflowY:'auto', padding:'2px 18px 40px',
          display:'flex', flexDirection:'column', gap:10 }}>
          {items.length ? items.map(it => (
            <ItemRow key={it.ru} item={it} kind={itemKind} color={color}
              isOn={!!learned[it.ru]} isActive={active === it.ru} expanded={expanded === it.ru}
              surface={'topic:' + topicId}
              onPlay={() => play(it)}
              onToggle={() => toggle(it, itemKind)}
              onExpand={() => setExpanded(e => (e === it.ru ? null : it.ru))} />
          )) : (
            <div style={{ textAlign:'center', fontFamily:'"Hanken Grotesk",system-ui', fontWeight:600, fontSize:14,
              color:'var(--muted)', padding:'40px 20px' }}>
              No {view} in this topic yet.</div>
          )}
        </div>
      </div>
    );
  }

  window.TopicDetail = TopicDetail;
})();
