// Topics.jsx — topics grid + topic detail

// Turn an uploaded file name into a clean, Title-Cased topic name.
function fileToTopicName(fn) {
  let s = (fn || '').replace(/\.[^.]+$/, '');
  s = s.replace(/\s*\(\d+\)\s*$/, '');
  s = s.replace(/[-_]+/g, ' ');
  s = s.replace(/\b[0-9a-f]{6,}\b/gi, ' ');
  s = s.replace(/\b(report|notes?|draft|final|copy|v\d+)\b/gi, ' ');
  s = s.replace(/\s+/g, ' ').trim();
  if (!s) return 'Imported topic';
  return s.replace(/\w\S*/g, (w) => w[0].toUpperCase() + w.slice(1));
}

// Generate real study-material content from the uploaded file's text via the
// built-in Claude helper. Returns a { summary, map, deck } object, or null.
// Robustly pull a JSON object out of a model reply, tolerating code fences,
// stray prose, and trailing commas.
function parseModelJSON(raw) {
  let s = String(raw || '').trim().replace(/^```(?:json)?/i, '').replace(/```$/i, '').trim();
  const i = s.indexOf('{'), j = s.lastIndexOf('}');
  if (i >= 0 && j > i) s = s.slice(i, j + 1);
  try { return JSON.parse(s); } catch (e) {}
  try { return JSON.parse(s.replace(/,\s*([}\]])/g, '$1')); } catch (e) {}
  return null;
}

// Generate ONE material type in its own focused call. Smaller, well-shaped JSON
// is far more reliable than asking for all three at once, and lets each material
// succeed independently. Everything is grounded ONLY in the supplied document.
async function genKind(kind, src, topicName, hasDoc) {
  if (!(window.claude && window.claude.complete)) return null;
  let schema;
  if (kind === 'summary') schema = '{"summary":{"head":"a specific descriptive title","intro":"a 2-4 sentence narrative introduction","secs":[ 4 to 6 objects {"h":"a heading specific to THIS material","b":"a detailed 3-5 sentence narrative that teaches the idea and weaves in concrete examples, names, numbers, definitions or short quotes"} ]}}';
  else if (kind === 'map') schema = '{"map":[ 7 to 10 objects {"label":"a specific named concept, term, person, date or example (1-4 words)","note":"a concrete detail including the actual number, date, name, mechanism or example (6-14 words)"} ]}';
  else schema = '{"deck":[ 6 to 8 objects {"q":"a specific question","a":"a precise, complete answer","label":"a short topic tag"} ]}';
  const ground = hasDoc ?
  'Use ONLY information found in the DOCUMENT below — every fact, name, number, date, example and quote MUST come from it. Do not add any outside knowledge, and do not invent details that are not in the document.' :
  'Use accurate, well-established facts strictly about "' + topicName + '". Do not drift to any unrelated subject.';
  const prompt = 'You are creating study material about "' + topicName + '". ' + ground +
  ' Return ONLY strictly valid minified JSON in EXACTLY this shape: ' + schema +
  '. Escape every inner double-quote as \\", put no line breaks inside string values, and add no markdown or commentary.' + (
  hasDoc ? '\n\nDOCUMENT:\n' + src.slice(0, 9000) : '');
  const attempt = async () => { try { return parseModelJSON(await window.claude.complete(prompt)); } catch (e) { return null; } };
  let o = await attempt();
  if (!o || typeof o !== 'object' || o[kind] == null) o = await attempt();
  return o ? o[kind] : null;
}

// Generate the selected study materials from a file's text (or, with no usable
// text, from well-established facts about the topic). Each type is generated in
// its own call and merged — so one failing type never blocks the others.
async function genFromFile(text, topicName, picks) {
  if (!(window.claude && window.claude.complete)) return null;
  const src = String(text || '').trim();
  const hasDoc = src.length >= 40;
  const jobs = [];
  if (picks.summary) jobs.push(genKind('summary', src, topicName, hasDoc).then((v) => ['summary', v]));
  if (picks.map) jobs.push(genKind('map', src, topicName, hasDoc).then((v) => ['map', v]));
  if (picks.deck) jobs.push(genKind('deck', src, topicName, hasDoc).then((v) => ['deck', v]));
  if (!jobs.length) return null;
  const out = {};
  (await Promise.all(jobs)).forEach(([k, v]) => { if (v != null) out[k] = v; });
  return Object.keys(out).length ? out : null;
}

// Lazily load pdf.js (only when a PDF is actually uploaded).
let _pdfjsPromise = null;
function loadPdfjs() {
  if (window.pdfjsLib) return Promise.resolve(window.pdfjsLib);
  if (_pdfjsPromise) return _pdfjsPromise;
  _pdfjsPromise = new Promise((res, rej) => {
    const s = document.createElement('script');
    s.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
    s.onload = () => { try { window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; } catch (e) {} res(window.pdfjsLib); };
    s.onerror = rej;
    document.head.appendChild(s);
  });
  return _pdfjsPromise;
}
async function extractPdfText(file) {
  const lib = await loadPdfjs();
  const buf = await file.arrayBuffer();
  const pdf = await lib.getDocument({ data: buf }).promise;
  let out = '';
  const n = Math.min(pdf.numPages, 30);
  for (let i = 1; i <= n; i++) { const pg = await pdf.getPage(i); const tc = await pg.getTextContent(); out += tc.items.map((it) => it.str).join(' ') + '\n'; }
  return out.trim();
}
// Pull readable text out of an uploaded file. Handles plain-text formats and
// PDFs; returns '' for formats we can't read in-browser (docx, images), so the
// caller falls back to topic-name-based generation.
async function extractFileText(file) {
  if (!file) return '';
  const name = (file.name || '').toLowerCase();
  const type = file.type || '';
  const isText = /\.(txt|md|markdown|csv|tsv|json|html?|xml|rtf|log|tex|srt|vtt)$/.test(name) || type.startsWith('text/') || type === 'application/json';
  if (isText) return await new Promise((res) => { const r = new FileReader(); r.onload = () => res(String(r.result || '')); r.onerror = () => res(''); r.readAsText(file); });
  if (/\.pdf$/.test(name) || type === 'application/pdf') { try { return await extractPdfText(file); } catch (e) { return ''; } }
  return '';
}

function TopicsScreen({ query, openTopic, filterDomain, setFilterDomain, onCapture, go, goBack, topics, onAddTopic, onAddMaterials, onEditTopic, onDeleteTopic, onAddDomain, domainV, toast }) {
  const all = topics || TOPICS;
  const q = (query || '').toLowerCase();
  const list = all.filter((t) =>
  (filterDomain === 'all' || t.domain === filterDomain) && (
  t.name.toLowerCase().includes(q) || domainById(t.domain).name.toLowerCase().includes(q))
  );
  const [adding, setAdding] = React.useState(false);
  const [editId, setEditId] = React.useState(null); // topic id being edited, or null = creating
  const [confirmDel, setConfirmDel] = React.useState(null); // topic pending delete
  const [name, setName] = React.useState('');
  const [dom, setDom] = React.useState(filterDomain !== 'all' ? filterDomain : DOMAINS[0].id);
  const [file, setFile] = React.useState(null);
  const [fileText, setFileText] = React.useState('');
  const [gsel, setGsel] = React.useState({ summary: true, map: true, deck: true });
  const [addingDom, setAddingDom] = React.useState(false); // custom-domain input open?
  const [domName, setDomName] = React.useState('');
  const [creating, setCreating] = React.useState(false); // generating-from-file wait state
  const [cStep, setCStep] = React.useState(0);
  const cTimersRef = React.useRef([]);
  const commitDomain = () => {
    const nm = domName.trim();
    if (!nm || !onAddDomain) { setAddingDom(false); setDomName(''); return; }
    const d = onAddDomain(nm);
    if (d) { setDom(d.id); if (toast) toast('Domain “' + d.name + '” added'); }
    setAddingDom(false); setDomName('');
  };
  const fileRef = React.useRef(null);
  const open = () => {setEditId(null);setName('');setDom(filterDomain !== 'all' ? filterDomain : DOMAINS[0].id);setFile(null);setFileText('');setGsel({ summary: true, map: true, deck: true });setAddingDom(false);setDomName('');if (fileRef.current) fileRef.current.value = '';setAdding(true);};
  const openEdit = (t) => {setEditId(t.id);setName(t.name);setDom(t.domain);setFile(null);setFileText('');setGsel({ summary: true, map: true, deck: true });setAddingDom(false);setDomName('');if (fileRef.current) fileRef.current.value = '';setAdding(true);};
  const save = () => {
    if (editId) {
      if (!name.trim()) return;
      onEditTopic(editId, { name: name.trim(), domain: dom });
      if (toast) toast('Topic updated');
      setAdding(false);
      return;
    }
    create();
  };
  const create = () => {
    const finalName = name.trim() || (file ? fileToTopicName(file.name) : '');
    if (!finalName) return;
    const t = onAddTopic(finalName, dom);
    if (!t) { setAdding(false); return; }
    const sel = ['summary', 'map', 'deck'].filter((k) => gsel[k]);
    const ids = { summary: 'm-' + t.id + '-sum', map: 'm-' + t.id + '-map', deck: 'm-' + t.id + '-deck' };
    const labelFor = { summary: finalName + ' — summary', map: finalName + ' — concept map', deck: finalName + ' — flashcards' };
    if (file && sel.length) {
      const mats = sel.map((k) => ({ id: ids[k], type: k, topic: t.id, title: labelFor[k], updated: 'just now', ...(k === 'deck' ? { cards: 6 } : {}) }));
      if (onAddMaterials) onAddMaterials(mats);
      if (window.claude && window.claude.complete) {
        // Stay on a generating screen and WAIT until all selected materials are
        // built from the file before navigating — so the user lands on real,
        // reviewable content, never empty placeholders.
        setCreating(true); setCStep(0);
        let genOk = false;
        cTimersRef.current = [setTimeout(() => setCStep(1), 700), setTimeout(() => setCStep(2), 1500), setTimeout(() => setCStep(3), 3200)];
        Promise.resolve(fileText || '').then((ft) => ft.trim() ? ft : extractFileText(file).catch(() => '')).then((ft) =>
        genFromFile(ft || '', finalName, gsel)).then((json) => {
          window.SUMMARIES = window.SUMMARIES || {};
          window.CONCEPT_MAPS = window.CONCEPT_MAPS || {};
          window.FLASHCARDS = window.FLASHCARDS || {};
          if (json) {
            if (gsel.summary && json.summary) { window.SUMMARIES[ids.summary] = { head: json.summary.head || finalName, intro: json.summary.intro || '', secs: (json.summary.secs || []).slice(0, 6), links: [{ label: file.name, file: true }] }; genOk = true; }
            if (gsel.map && Array.isArray(json.map) && json.map.length) { window.CONCEPT_MAPS[t.id] = json.map.slice(0, 10).map((c) => ({ label: c.label || '', note: c.note || '' })); genOk = true; }
            if (gsel.deck && Array.isArray(json.deck) && json.deck.length) { window.FLASHCARDS[ids.deck] = json.deck.slice(0, 8).map((c) => ({ q: c.q || '', a: c.a || '', label: c.label || 'Card' })); genOk = true; }
          }
          try { window.dispatchEvent(new CustomEvent('pl-generated', { detail: { topic: t.id } })); } catch (e) {}
        }).catch(() => {}).then(() => {
          (cTimersRef.current || []).forEach(clearTimeout);
          setCreating(false); setAdding(false); setCStep(0);
          if (toast) toast(genOk ? 'Materials ready for ' + finalName : 'Created “' + finalName + '” — tap Regenerate if a material looks generic');
          openTopic(t.id);
        });
        return;
      }
      if (toast) toast('Generated ' + mats.length + ' material' + (mats.length > 1 ? 's' : '') + ' from ' + file.name);
    }
    setAdding(false);
    openTopic(t.id);
  };
  return (
    <div className="content-pad fade-up topics-page">
      <div className="page-head">
        <div>
          <p className="eyebrow">Library</p>
          <h1>My Collection</h1>
          <p className="sub">{all.length} topics across {DOMAINS.length} domains · synced from all connected applications</p>
        </div>
        <div className="actions" style={{ display: 'flex', gap: 10 }}>
          <button className="btn" onClick={goBack}><Icon name="arrow-left" size={16} /> Back</button>
          <button className="btn" onClick={() => go('map')}><Icon name="home" size={16} /> Learning map</button>
          <button className="btn primary" onClick={open}><Icon name="plus" size={16} /> Add topic</button>
        </div>
      </div>

      <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 22 }}>
        <button className={'chip' + (filterDomain === 'all' ? ' on' : '')} onClick={() => setFilterDomain('all')}>All domains</button>
        {DOMAINS.map((d) =>
        <button key={d.id} className={'chip' + (filterDomain === d.id ? ' on' : '')} onClick={() => setFilterDomain(d.id)}>
            <span style={{ width: 8, height: 8, borderRadius: 99, background: d.accent }} /> {d.name}
          </button>
        )}
      </div>

      {list.length === 0 ?
      <EmptyHint icon="search" title="No topics match" sub="Try a different search or clear the domain filter."
      action={<button className="btn" onClick={() => setFilterDomain('all')}>Clear filter</button>} /> :

      <div className="grid cards-3">
          {list.map((t) => <TopicCard key={t.id} topic={t} onOpen={openTopic} onEdit={openEdit} onDelete={setConfirmDel} />)}
          <button className="card hover" onClick={open} style={{ display: 'flex', flexDirection: 'column',
          alignItems: 'center', justifyContent: 'center', gap: 9, minHeight: 150, border: '1.5px dashed var(--line-strong)',
          background: 'transparent', cursor: 'pointer', color: 'var(--ink-3)' }}>
            <span className="itile" style={{ width: 40, height: 40, borderRadius: 11, background: 'var(--surface-2)' }}><Icon name="plus" size={20} /></span>
            <span style={{ fontSize: 13.5, fontWeight: 600 }}>Add topic</span>
          </button>
        </div>
      }

      {adding &&
      <div className="scrim" onMouseDown={(e) => {if (!creating && e.target === e.currentTarget) setAdding(false);}}>
          <div className="modal" style={{ maxWidth: 480 }}>
            <div className="mhead">
              <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                <span className="itile" style={{ width: 32, height: 32, borderRadius: 9, background: 'var(--surface-2)', color: 'var(--ink-2)' }}><Icon name="layers" size={17} /></span>
                <span style={{ fontWeight: 600, fontSize: 16 }}>{creating ? 'Generating materials…' : editId ? 'Edit topic' : 'New topic'}</span>
              </div>
              <button className="icon-btn" style={{ width: 32, height: 32, border: 'none', opacity: creating ? 0.4 : 1, pointerEvents: creating ? 'none' : 'auto' }} onClick={() => {if (!creating) setAdding(false);}}><Icon name="x" size={18} /></button>
            </div>
            {creating ?
            <div className="mbody" style={{ padding: '34px 22px' }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 13, marginBottom: 22 }}>
                <span className="itile" style={{ width: 42, height: 42, borderRadius: 12, background: 'var(--sky)', color: 'var(--blue-ink)' }}><Icon name="sparkles" size={21} /></span>
                <div>
                  <div style={{ fontSize: 15.5, fontWeight: 600 }}>Building “{(name.trim() || (file ? fileToTopicName(file.name) : 'topic'))}”</div>
                  <div className="mono" style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 2 }}>{['Reading ' + (file ? file.name : 'your file') + '…', 'Pulling out the key concepts & details…', 'Writing your study materials…', 'Finalizing — almost ready…'][Math.min(cStep, 3)]}</div>
                </div>
              </div>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 11 }}>
                {['Reading the file…', 'Pulling out key concepts…', 'Writing your materials…', 'Finalizing — almost ready…'].map((label, i) =>
                <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 11 }}>
                    {i < cStep ? <Icon name="check" size={16} style={{ color: 'var(--positive)' }} /> :
                  i === cStep ? <span style={{ width: 16, height: 16, borderRadius: 99, border: '2px solid var(--blue)', borderTopColor: 'transparent', animation: 'spin .7s linear infinite' }} /> :
                  <span style={{ width: 16, height: 16, borderRadius: 99, border: '2px solid var(--line-strong)' }} />}
                    <span style={{ fontSize: 12.5, color: i <= cStep ? 'var(--ink-2)' : 'var(--ink-4)' }}>{label}</span>
                  </div>
                )}
              </div>
              <div className="t-small" style={{ marginTop: 16, textAlign: 'center', color: 'var(--ink-3)' }}>Generating all your materials from the file can take a little longer — hang tight, we’ll open them when they’re ready to review.</div>
            </div> :
            <React.Fragment>
            <div className="mbody">
              <label className="field-label">Topic name</label>
              <input className="input" autoFocus value={name} onChange={(e) => setName(e.target.value)}
            onKeyDown={(e) => {if (e.key === 'Enter') save();}} placeholder="e.g. Organic Chemistry" />
              <label className="field-label" style={{ marginTop: 16 }}>Domain</label>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
                {DOMAINS.map((d) =>
              <button key={d.id} className={'chip' + (dom === d.id ? ' on' : '')} onClick={() => setDom(d.id)}>
                    <span style={{ width: 8, height: 8, borderRadius: 99, background: d.accent }} /> {d.name}
                  </button>
              )}
                {addingDom ?
                <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                  <input className="input" autoFocus value={domName} onChange={(e) => setDomName(e.target.value)}
                    onKeyDown={(e) => {if (e.key === 'Enter') {e.preventDefault();commitDomain();} else if (e.key === 'Escape') {setAddingDom(false);setDomName('');}}}
                    placeholder="New domain name" style={{ height: 32, padding: '0 10px', fontSize: 13, width: 160 }} />
                  <button type="button" className="chip on" onClick={commitDomain} disabled={!domName.trim()} title="Add domain"><Icon name="check" size={13} /></button>
                  <button type="button" className="icon-btn" style={{ width: 28, height: 28, border: 'none', flex: 'none' }} onClick={() => {setAddingDom(false);setDomName('');}}><Icon name="x" size={14} /></button>
                </div> :
                <button type="button" className="chip" onClick={() => setAddingDom(true)} style={{ borderStyle: 'dashed' }}><Icon name="plus" size={13} /> Add domain</button>
                }
              </div>

              <label className="field-label" style={{ marginTop: 18 }}>Generate from a file <span style={{ color: 'var(--ink-4)', fontWeight: 400 }}>(optional)</span></label>
              {editId && <div className="t-small" style={{ marginTop: -2, marginBottom: 2 }}>File generation is available when creating a topic. Use <strong>Generate material</strong> on the topic to add more.</div>}
              {!editId && <React.Fragment>
              <input type="file" ref={fileRef} style={{ display: 'none' }} accept=".pdf,.doc,.docx,.txt,.md,.png,.jpg,.jpeg"
                onChange={(e) => {const f = e.target.files && e.target.files[0];if (f) {setFile(f);if (!name.trim()) setName(fileToTopicName(f.name));setFileText('');extractFileText(f).then((txt) => setFileText(txt || '')).catch(() => setFileText(''));}}} />
              {!file ?
              <button type="button" onClick={() => fileRef.current && fileRef.current.click()}
                style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6, padding: '20px 16px', borderRadius: 12, border: '1.5px dashed var(--line-strong)', background: 'var(--surface-2)', cursor: 'pointer', color: 'var(--ink-3)' }}>
                <Icon name="download" size={20} style={{ transform: 'rotate(180deg)', color: 'var(--blue-ink)' }} />
                <span style={{ fontSize: 13.5, fontWeight: 600, color: 'var(--ink-2)' }}>Upload a file</span>
                <span style={{ fontSize: 11.5, textAlign: 'center' }}>PDF, Word, text, or an image — we’ll read it and build your materials</span>
              </button> :
              <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px', borderRadius: 12, border: '1px solid var(--line)', background: 'var(--surface-2)' }}>
                <span className="itile" style={{ width: 30, height: 30, borderRadius: 8, background: 'var(--paper)', color: 'var(--blue-ink)', flex: 'none' }}><Icon name="book" size={16} /></span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 13.5, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{file.name}</div>
                  <div className="mono" style={{ fontSize: 11, color: 'var(--ink-3)' }}>{Math.max(1, Math.round((file.size || 0) / 1024))} KB · ready to generate</div>
                </div>
                <button className="icon-btn" style={{ width: 28, height: 28, border: 'none', flex: 'none' }} onClick={() => {setFile(null);setFileText('');if (fileRef.current) fileRef.current.value = '';}}><Icon name="x" size={15} /></button>
              </div>
              }
              {file &&
              <React.Fragment>
                <label className="field-label" style={{ marginTop: 16 }}>Materials to generate</label>
                <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
                  {[['summary', 'Summary', 'book'], ['map', 'Concept map', 'map'], ['deck', 'Flashcards', 'cards']].map(([k, label, ic]) =>
                  <button key={k} type="button" className={'chip' + (gsel[k] ? ' on' : '')} onClick={() => setGsel((s) => ({ ...s, [k]: !s[k] }))}>
                      <Icon name={gsel[k] ? 'check' : ic} size={13} /> {label}
                    </button>
                  )}
                </div>
                <div className="t-small" style={{ marginTop: 8 }}>{Object.values(gsel).filter(Boolean).length === 0 ? 'Select at least one material to generate.' : 'Pick any combination — all three are on by default.'}</div>
              </React.Fragment>
              }
              </React.Fragment>}
            </div>
            <div className="mfoot">
              <button className="btn" onClick={() => setAdding(false)}>Cancel</button>
              <button className="btn primary" disabled={editId ? !name.trim() : ((!name.trim() && !file) || (file && Object.values(gsel).filter(Boolean).length === 0))} onClick={save}><Icon name="check" size={16} /> {editId ? 'Save changes' : file ? 'Create & generate' : 'Create topic'}</button>
            </div>
            </React.Fragment>}
          </div>
        </div>
      }

      {confirmDel &&
      <div className="scrim" onMouseDown={(e) => {if (e.target === e.currentTarget) setConfirmDel(null);}}>
          <div className="modal" style={{ maxWidth: 400 }}>
            <div className="mhead">
              <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                <span className="itile" style={{ width: 32, height: 32, borderRadius: 9, background: 'color-mix(in srgb,var(--critical) 14%, var(--paper))', color: 'var(--critical)' }}><Icon name="trash" size={17} /></span>
                <span style={{ fontWeight: 600, fontSize: 16 }}>Delete topic</span>
              </div>
              <button className="icon-btn" style={{ width: 32, height: 32, border: 'none' }} onClick={() => setConfirmDel(null)}><Icon name="x" size={18} /></button>
            </div>
            <div className="mbody">
              <p className="t-body" style={{ fontSize: 14, lineHeight: 1.6, margin: 0 }}>
                Delete <strong>{confirmDel.name}</strong>? This also removes its generated materials. This can’t be undone.
              </p>
            </div>
            <div className="mfoot">
              <button className="btn" onClick={() => setConfirmDel(null)}>Cancel</button>
              <button className="btn" style={{ background: 'var(--critical)', color: '#fff', borderColor: 'var(--critical)' }}
                onClick={() => { const nm = confirmDel.name; onDeleteTopic(confirmDel.id); setConfirmDel(null); if (toast) toast('Deleted “' + nm + '”'); }}>
                <Icon name="trash" size={15} /> Delete topic</button>
            </div>
          </div>
        </div>
      }
    </div>);

}

// Per-topic workspace: uploaded files + personal notes (typed or dictated).
// Replaces the old external "linked notes" card. Persists per topic in localStorage.
function TopicWorkspace({ topicId }) {
  const FK = 'pl-topic-files-' + topicId;
  const NK = 'pl-topic-notes-' + topicId;
  const [files, setFiles] = React.useState(() => { try { return JSON.parse(localStorage.getItem(FK) || '[]'); } catch (e) { return []; } });
  const [notes, setNotes] = React.useState(() => { try { return JSON.parse(localStorage.getItem(NK) || '[]'); } catch (e) { return []; } });
  const [text, setText] = React.useState('');
  const [listening, setListening] = React.useState(false);
  const [micOk, setMicOk] = React.useState(true);
  const recRef = React.useRef(null);
  const baseRef = React.useRef('');
  const fileRef = React.useRef(null);
  const saveFiles = (list) => { setFiles(list); try { localStorage.setItem(FK, JSON.stringify(list)); } catch (e) {} try { window.dispatchEvent(new CustomEvent('pl-files', { detail: { topicId } })); } catch (e) {} };
  const saveNotes = (list) => { setNotes(list); try { localStorage.setItem(NK, JSON.stringify(list)); } catch (e) {} };
  React.useEffect(() => () => { try { recRef.current && recRef.current.stop(); } catch (e) {} }, []);
  // The Mastery panel's "Upload" button asks us to open the file picker.
  React.useEffect(() => {
    const onOpen = (e) => { if (e.detail && e.detail.topicId === topicId && fileRef.current) fileRef.current.click(); };
    window.addEventListener('pl-open-upload', onOpen);
    return () => window.removeEventListener('pl-open-upload', onOpen);
  }, [topicId]);

  const onPick = (e) => {
    const picked = Array.from(e.target.files || []);
    if (!picked.length) return;
    const add = picked.map((f, k) => ({ id: 'f' + Date.now() + k, name: f.name, size: f.size, type: f.type || '', added: Date.now(), text: '' }));
    saveFiles([...add, ...files]);
    if (fileRef.current) fileRef.current.value = '';
    // Extract each file's text in the background so generated materials can be
    // grounded in the file's actual content (not just its name). Store a capped
    // copy on the file record; update once ready.
    if (window.extractFileText) {
      picked.forEach((f, k) => {
        window.extractFileText(f).then((txt) => {
          if (!txt) return;
          setFiles((cur) => {
            const next = cur.map((x) => x.id === add[k].id ? { ...x, text: String(txt).slice(0, 12000) } : x);
            try { localStorage.setItem(FK, JSON.stringify(next)); } catch (e) {}
            try { window.dispatchEvent(new CustomEvent('pl-files', { detail: { topicId } })); } catch (e) {}
            return next;
          });
        }).catch(() => {});
      });
    }
  };
  const removeFile = (id) => saveFiles(files.filter((f) => f.id !== id));

  const toggleMic = () => {
    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SR) { setMicOk(false); return; }
    if (listening) { try { recRef.current && recRef.current.stop(); } catch (e) {} return; }
    try {
      const rec = new SR();
      rec.continuous = true; rec.interimResults = true; rec.lang = 'en-US';
      baseRef.current = text.trim() ? text.trim() + ' ' : '';
      rec.onstart = () => { setListening(true); setMicOk(true); };
      rec.onresult = (ev) => { let txt = ''; for (let i = 0; i < ev.results.length; i++) txt += ev.results[i][0].transcript; setText((baseRef.current + txt).replace(/\s+/g, ' ').replace(/^\s+/, '')); };
      rec.onerror = (ev) => { if (ev.error === 'not-allowed' || ev.error === 'service-not-allowed') setMicOk(false); setListening(false); };
      rec.onend = () => { setListening(false); recRef.current = null; };
      recRef.current = rec; rec.start();
    } catch (e) { setListening(false); }
  };

  const saveNote = () => {
    const v = text.trim(); if (!v) return;
    try { recRef.current && recRef.current.stop(); } catch (e) {}
    saveNotes([{ id: 'n' + Date.now(), text: v, created: Date.now() }, ...notes]);
    setText(''); baseRef.current = '';
  };
  const removeNote = (id) => saveNotes(notes.filter((n) => n.id !== id));

  const fmtSize = (b) => b == null ? '' : b < 1024 ? b + ' B' : b < 1048576 ? (b / 1024).toFixed(0) + ' KB' : (b / 1048576).toFixed(1) + ' MB';
  const fmtWhen = (ts) => { const d = Math.floor((Date.now() - ts) / 1000); if (d < 60) return 'just now'; if (d < 3600) return Math.floor(d / 60) + 'm ago'; if (d < 86400) return Math.floor(d / 3600) + 'h ago'; return new Date(ts).toLocaleDateString(); };
  const total = files.length + notes.length;

  return (
    <div className="card pad">
      <div className="section-title">
        <h2><Icon name="book" size={17} style={{ color: 'var(--ink-3)' }} /> Files &amp; notes <span className="count-pill">{total}</span></h2>
        <button className="btn sm" onClick={() => fileRef.current && fileRef.current.click()}><Icon name="plus" size={15} /> Upload file</button>
      </div>
      <input type="file" ref={fileRef} multiple style={{ display: 'none' }} accept=".pdf,.doc,.docx,.txt,.md,.png,.jpg,.jpeg,.ppt,.pptx,.csv" onChange={onPick} />

      {files.length > 0 &&
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 14 }}>
        {files.map((f) =>
        <div key={f.id} className="lrow" style={{ alignItems: 'center' }}>
            <span className="thumb" style={{ width: 34, height: 34, background: 'var(--surface-2)', color: 'var(--blue-ink)' }}><Icon name="book" size={16} /></span>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 14, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{f.name}</div>
              <div className="mono" style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 2 }}>{[fmtSize(f.size), fmtWhen(f.added)].filter(Boolean).join(' · ')}</div>
            </div>
            <button className="icon-btn" style={{ width: 30, height: 30, border: 'none', flex: 'none' }} title="Remove file" onClick={() => removeFile(f.id)}><Icon name="trash" size={15} /></button>
          </div>
        )}
      </div>
      }

      <div style={{ border: '1px solid var(--line)', borderRadius: 'var(--r-md)', background: 'var(--surface-2)', padding: 10 }}>
        <textarea className="input" value={text} onChange={(e) => setText(e.target.value)} rows={3}
          placeholder={listening ? 'Listening… speak your note' : 'Write a personal note for this topic…'}
          style={{ resize: 'vertical', minHeight: 64, background: 'var(--paper)' }} />
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 8 }}>
          <button className={'btn sm' + (listening ? ' blue' : '')} onClick={toggleMic} disabled={!micOk}
            title={!micOk ? 'Voice input unavailable in this browser' : listening ? 'Stop dictation' : 'Dictate a note'}>
            <Icon name="mic" size={15} /> {listening ? 'Stop' : 'Speak'}</button>
          {text.trim() && <button className="btn sm" onClick={() => { setText(''); baseRef.current = ''; }}>Clear</button>}
          <button className="btn blue sm" style={{ marginLeft: 'auto' }} disabled={!text.trim()} onClick={saveNote}><Icon name="check" size={15} /> Save note</button>
        </div>
        {listening && <div className="rac-dictate" style={{ padding: '8px 2px 0' }}><span className="rac-dictate-dot" /> Listening… speak your note, then tap Stop or Save.</div>}
        {!micOk && <div className="rac-dictate err" style={{ padding: '8px 2px 0' }}>Voice input isn’t available in this browser — type your note instead.</div>}
      </div>

      {notes.length > 0 &&
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 14 }}>
        {notes.map((n) =>
        <div key={n.id} className="goal-row" style={{ padding: '12px 14px', display: 'flex', alignItems: 'flex-start', gap: 11 }}>
            <span className="itile" style={{ width: 30, height: 30, borderRadius: 8, background: 'color-mix(in srgb,var(--blue) 12%, var(--paper))', color: 'var(--blue-ink)', flex: 'none' }}><Icon name="edit" size={15} /></span>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 14, lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>{n.text}</div>
              <div className="mono" style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 4 }}>{fmtWhen(n.created)}</div>
            </div>
            <button className="icon-btn" style={{ width: 30, height: 30, border: 'none', flex: 'none' }} title="Delete note" onClick={() => removeNote(n.id)}><Icon name="trash" size={15} /></button>
          </div>
        )}
      </div>
      }

      {total === 0 &&
      <div style={{ textAlign: 'center', color: 'var(--ink-3)', fontSize: 13, marginTop: 14 }}>No files or notes yet — upload a file or jot a quick note above.</div>
      }
    </div>);

}

function TopicDetailScreen({ topicId, topics, materials, openMaterial, openGenerate, go, toast }) {
  const t = topics && topics.find((x) => x.id === topicId) || topicById(topicId);
  const d = domainById(t.domain);
  const mats = materials.filter((m) => m.topic === topicId);
  const notes = NOTES[topicId] || FALLBACK_NOTES;
  const ms = (window.useMastery ? window.useMastery(topicId) : { pct: t.progress });
  const [matFilter, setMatFilter] = React.useState('all');
  const shown = matFilter === 'all' ? mats : mats.filter((m) => m.type === matFilter);

  return (
    <div className="content-pad fade-up topics-page">
      <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 14 }}>
        <Icon name="arrow-left" size={16} style={{ color: 'var(--ink-3)' }} />
        <button className="btn ghost sm" style={{ paddingLeft: 4 }} onClick={() => go('map')}>Learning map</button>
        <span style={{ color: 'var(--ink-4)', fontSize: 13 }}>or</span>
        <button className="btn ghost sm" onClick={() => go('topics')}>My Collection</button>
        <span style={{ color: 'var(--ink-4)', fontSize: 13 }}>or</span>
        <button className="btn ghost sm" onClick={() => go('materials')}>Study Materials</button>
      </div>

      <div className="page-head" style={{ marginBottom: 18 }}>
        <div>
          <p className="eyebrow" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <span style={{ width: 9, height: 9, borderRadius: 99, background: d.accent }} /> {d.name}</p>
          <h1>{t.name}</h1>
        </div>
        <button className="btn blue lg" onClick={() => openGenerate(topicId)}>
          <Icon name="sparkles" size={17} /> Generate material
        </button>
      </div>

      {/* progress panel */}
      <div className="card pad" style={{ marginBottom: 22, display: 'flex', alignItems: 'center', gap: 24, flexWrap: 'wrap' }}>
        {[['Notes', t.notes, 'link'], ['Materials', mats.length, 'cards'], ['Cards due', t.due, 'clock'], ['Mastery', ms.pct + '%', 'trending']].map(([lab, val, ic], i) =>
        <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 11, fontSize: "6px", textAlign: "center" }}>
            <span className="itile" style={{ width: 34, height: 34, borderRadius: 9, background: 'var(--surface-2)', color: 'var(--ink-2)' }}><Icon name={ic} size={17} /></span>
            <div style={{ lineHeight: 1.2 }}>
              <div style={{ fontSize: 19, fontWeight: 600, letterSpacing: '-0.02em' }}>{val}</div>
              <div className="mono" style={{ color: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: '.04em', fontSize: "10px" }}>{lab}</div>
            </div>
            {i < 3 && <div style={{ width: 1, height: 34, background: 'var(--line)', marginLeft: 14 }} />}
          </div>
        )}
        <div style={{ flex: 1, minWidth: 180 }}><ProgressBar value={ms.pct} lg /></div>
      </div>

      {window.MasteryPanel && <div style={{ marginBottom: 22 }}><MasteryPanel topicId={topicId} topic={t} mats={mats} openMaterial={openMaterial} openGenerate={openGenerate} toast={toast} /></div>}

      <div className="col-split">
        {/* files & personal notes, then the original synced Notion notes */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 22 }}>
          <TopicWorkspace topicId={topicId} />
          <div className="card pad">
            <div className="section-title">
              <h2><Icon name="link" size={17} style={{ color: 'var(--ink-3)' }} /> Linked notes <span className="count-pill">{t.notes}</span></h2>
              <span className="tag" style={{ background: 'rgba(46,122,58,.12)', color: 'var(--positive)' }}>
                <span className="dot" style={{ background: 'var(--positive)' }} /> synced</span>
            </div>
            {notes.map((n, i) =>
            <div key={i} className="lrow" style={{ alignItems: 'flex-start' }}>
                <span className="thumb" style={{ width: 34, height: 34, background: 'var(--surface-2)', color: 'var(--ink-3)' }}><Icon name="book" size={17} /></span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 14, fontWeight: 600 }}>{n.title}</div>
                  <div style={{ fontSize: 12.5, color: 'var(--ink-2)', marginTop: 2, lineHeight: 1.4,
                  overflow: 'hidden', textOverflow: 'ellipsis', display: '-webkit-box', WebkitLineClamp: 1, WebkitBoxOrient: 'vertical' }}>{n.excerpt}</div>
                </div>
                <span className="mono" style={{ fontSize: 11, color: 'var(--ink-3)' }}>{n.updated}</span>
              </div>
            )}
            <a className="btn ghost block" href="https://www.notion.com/" target="_blank" rel="noreferrer" style={{ marginTop: 8 }}>Open in Notion <Icon name="arrow-up-right" size={15} /></a>
          </div>
        </div>

        {/* generated materials */}
        <div className="card pad">
          <div className="section-title">
            <h2><Icon name="cards" size={17} style={{ color: 'var(--ink-3)' }} /> Generated materials <span className="count-pill">{mats.length}</span></h2>
            <div className="segmented">
              {[['all', 'All'], ['summary', 'Summaries'], ['map', 'Maps'], ['deck', 'Decks']].map(([id, lab]) =>
              <button key={id} className={matFilter === id ? 'on' : ''} onClick={() => setMatFilter(id)}>{lab}</button>
              )}
            </div>
          </div>
          {shown.length === 0 ?
          <div style={{ textAlign: 'center', padding: '20px 0', color: 'var(--ink-3)', fontSize: 13.5 }}>No materials of this type yet.</div> :

          <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              {shown.map((m) =>
            <button key={m.id} className="card tight hover" onClick={() => openMaterial(m.id)}
            style={{ display: 'flex', alignItems: 'center', gap: 13, textAlign: 'left', borderRadius: 'var(--r-md)' }}>
                  <MatTile type={m.type} size={38} />
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 14, fontWeight: 600, color: "var(--ink)" }}>{m.title}</div>
                    <div className="mono" style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 2 }}>
                      {MAT_TYPE[m.type].label}{m.cards ? ` · ${m.cards} cards` : ''} · {m.updated}</div>
                  </div>
                  <Icon name="chevron-right" size={17} style={{ color: 'var(--ink-4)' }} />
                </button>
            )}
            </div>
          }

          {/* prominent generate */}
          <div style={{ marginTop: 16, padding: 16, borderRadius: 'var(--r-md)', border: '1.5px dashed var(--line-strong)',
            background: 'var(--surface-2)', textAlign: 'center' }}>
            <div style={{ fontSize: 13.5, fontWeight: 600, marginBottom: 11 }}>Turn these notes into something to study</div>
            <div style={{ display: 'flex', gap: 8, justifyContent: 'center', flexWrap: 'wrap' }}>
              {['summary', 'map', 'deck'].map((ty) =>
              <button key={ty} className="chip" onClick={() => openGenerate(topicId, ty)}>
                  <Icon name={MAT_TYPE[ty].icon} size={15} style={{ color: MAT_TYPE[ty].tint }} /> {MAT_TYPE[ty].label}
                </button>
              )}
            </div>
          </div>
        </div>
      </div>
    </div>);

}

Object.assign(window, { TopicsScreen, TopicDetailScreen, genFromFile, extractFileText });