// astroparse — root app: screens, session persistence, LLM generation, tweaks

const AP_SESSION_KEY = 'astroparse_session_v2';

const AP_TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "paperPalette": ["#f6f1e6", "#2b2418", "#9a3b20"],
  "bodySize": 19,
  "dropCaps": true,
  "justify": true,
  "showScores": true,
  "notesSide": "right"
}/*EDITMODE-END*/;

const AP_MODE_INSTRUCTIONS = {
  history: 'Provide HISTORICAL CONTEXT: how this claim or approach emerged in the field, which earlier works set it up, and how thinking shifted over time.',
  referee: 'Act as a constructive but rigorous REFEREE: identify the strongest assumption, an unstated caveat, or a check the authors should report. Be specific, not generic.',
  literature: 'Offer OBSERVATIONS FROM THE RELATED LITERATURE: how the retrieved works support, contradict, or extend what this paragraph claims. Compare numbers where possible.',
  explainer: 'Write a PLAIN-LANGUAGE EXPLAINER for a beginning graduate student: unpack jargon and explain why this paragraph matters, without dumbing down.',
  methods: 'Provide a METHODS COMPARISON: contrast the technique in this paragraph with the approaches used in the retrieved works, noting trade-offs.',
};

function buildPrompt(para, modeId, litList) {
  const litBlock = litList
    .map((l, i) => `[${i + 1}] ${l.short} — "${l.title}" (${l.journal}). ${l.abstract}`)
    .join('\n');
  return (
    'You are an erudite astronomical annotator writing a marginal note beside a paragraph of a research paper, ' +
    'in the tradition of an annotated encyclopedia. Scholarly, precise, a little wry. ' +
    'Write 55–85 words, a single paragraph, no markdown, no preamble. ' +
    'Refer to the retrieved works by bracketed number, e.g. [1], where relevant.\n\n' +
    AP_MODE_INSTRUCTIONS[modeId] + '\n\n' +
    'PARAGRAPH (from section "' + para.section + '"):\n' + para.text + '\n\n' +
    'RETRIEVED LITERATURE (via Pathfinder):\n' + litBlock + '\n\n' +
    'Marginal note:'
  );
}

function App() {
  const data = window.ASTROPARSE_DATA;
  const [t, setTweak] = useTweaks(AP_TWEAK_DEFAULTS);
  const [screen, setScreen] = React.useState('upload'); // upload | pipeline | reader
  const [fileName, setFileName] = React.useState('Mowla_Iyer_2024_FireflySparkle.pdf');
  const [noteModes, setNoteModes] = React.useState({ ...data.defaultModes });
  const [commentary, setCommentary] = React.useState(() => {
    const init = {};
    Object.entries(data.cannedCommentary).forEach(([k, v]) => { init[k] = { ...v, source: 'canned' }; });
    return init;
  });
  const [generating, setGenerating] = React.useState({});
  const [toast, setToast] = React.useState(null);
  const [annotator, setAnnotator] = React.useState(apLoadAnnotator);
  const [settingsOpen, setSettingsOpen] = React.useState(false);
  const annotatorRef = React.useRef(annotator);
  annotatorRef.current = annotator;
  const inflight = React.useRef(new Set());

  /* ---------- restore session from localStorage ---------- */
  React.useEffect(() => {
    try {
      const raw = localStorage.getItem(AP_SESSION_KEY);
      if (raw) {
        const s = JSON.parse(raw);
        if (s && s.commentary && s.screen === 'reader') {
          setCommentary(s.commentary);
          setNoteModes(s.noteModes || { ...data.defaultModes });
          setFileName(s.fileName || 'Mowla_Iyer_2024_FireflySparkle.pdf');
          setScreen('reader');
        }
      }
    } catch (e) { /* ignore corrupt session */ }
  }, []);

  /* ---------- autosave ---------- */
  React.useEffect(() => {
    if (screen !== 'reader') return;
    const s = { version: 1, savedAt: new Date().toISOString(), fileName, screen, noteModes, commentary };
    try { localStorage.setItem(AP_SESSION_KEY, JSON.stringify(s)); } catch (e) {}
  }, [screen, fileName, noteModes, commentary]);

  /* ---------- LLM generation ---------- */
  const generate = React.useCallback(async (paraId, modeId, force) => {
    const key = paraId + ':' + modeId;
    if (inflight.current.has(key)) return;
    inflight.current.add(key);
    setGenerating((g) => ({ ...g, [key]: true }));
    const para = data.paper.paragraphs.find((p) => p.id === paraId);
    const litIds = data.litByPara[paraId] || [];
    const litList = litIds.map((id) => data.litPapers[id]).filter(Boolean);
    try {
      const text = await apComplete(annotatorRef.current, buildPrompt(para, modeId, litList));
      setCommentary((c) => ({ ...c, [key]: { text: String(text).trim(), cites: litIds, source: 'llm' } }));
    } catch (e) {
      setCommentary((c) => ({ ...c, [key]: { error: true, message: e && e.message, cites: litIds } }));
    } finally {
      inflight.current.delete(key);
      setGenerating((g) => { const n = { ...g }; delete n[key]; return n; });
    }
  }, []);

  const setMode = React.useCallback((paraId, modeId) => {
    setNoteModes((m) => (m[paraId] === modeId ? m : { ...m, [paraId]: modeId }));
    setCommentary((c) => {
      const key = paraId + ':' + modeId;
      if (!c[key]) generate(paraId, modeId);
      return c;
    });
  }, [generate]);

  const regenerate = React.useCallback((paraId, modeId) => generate(paraId, modeId, true), [generate]);

  /* ---------- session export / import ---------- */
  const exportSession = () => {
    const s = { version: 1, savedAt: new Date().toISOString(), fileName, screen: 'reader', noteModes, commentary, paper: data.paper };
    const blob = new Blob([JSON.stringify(s, null, 2)], { type: 'application/json' });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = 'astroparse-session.json';
    a.click();
    URL.revokeObjectURL(a.href);
    flash('session written to astroparse-session.json');
  };

  const importSession = (text) => {
    try {
      const s = JSON.parse(text);
      if (!s || !s.commentary) throw new Error('bad session');
      setCommentary(s.commentary);
      setNoteModes(s.noteModes || { ...data.defaultModes });
      setFileName(s.fileName || 'restored-session.pdf');
      setScreen('reader');
      flash('session restored');
    } catch (e) {
      flash('could not read that session file');
    }
  };

  const flash = (msg) => {
    setToast(msg);
    setTimeout(() => setToast(null), 2600);
  };

  const reset = () => {
    setScreen('upload');
    setNoteModes({ ...data.defaultModes });
    setCommentary(() => {
      const init = {};
      Object.entries(data.cannedCommentary).forEach(([k, v]) => { init[k] = { ...v, source: 'canned' }; });
      return init;
    });
    try { localStorage.removeItem(AP_SESSION_KEY); } catch (e) {}
  };

  const importRef = React.useRef(null);

  const toolbar = (
    <div className="ap-toolbar">
      <span className="ap-toolbar-mark">astroparse</span>
      <span className="ap-toolbar-file">{fileName}</span>
      <span className="ap-toolbar-spacer"></span>
      <button className="ap-textbtn" onClick={() => setSettingsOpen(true)}>
        annotator: {AP_PROVIDERS[annotator.provider].label.toLowerCase()}
      </button>
      <button className="ap-textbtn" onClick={exportSession}>save session &darr;</button>
      <button className="ap-textbtn" onClick={() => importRef.current && importRef.current.click()}>load</button>
      <button className="ap-textbtn" onClick={reset}>new manuscript</button>
      <input
        ref={importRef} type="file" accept="application/json" style={{ display: 'none' }}
        onChange={(e) => {
          const f = e.target.files[0]; if (!f) return;
          const r = new FileReader();
          r.onload = () => importSession(r.result);
          r.readAsText(f);
          e.target.value = '';
        }}
      />
    </div>
  );

  const pal = t.paperPalette || AP_TWEAK_DEFAULTS.paperPalette;
  const rootStyle = {
    '--paper': pal[0], '--ink': pal[1], '--rubric': pal[2],
    '--body-size': t.bodySize + 'px',
  };

  return (
    <div className="ap-root" style={rootStyle}>
      {screen === 'upload' && (
        <UploadScreen
          onFile={(name) => { setFileName(name); setScreen('pipeline'); }}
          onSample={() => { setFileName('Mowla_Iyer_2024_FireflySparkle.pdf'); setScreen('pipeline'); }}
          onImportSession={importSession}
        />
      )}
      {screen === 'pipeline' && <PipelineScreen fileName={fileName} onDone={() => setScreen('reader')} />}
      {screen === 'reader' && (
        <Reader
          paper={data.paper}
          commentary={commentary}
          noteModes={noteModes}
          generating={generating}
          onSetMode={setMode}
          onRegenerate={regenerate}
          tweaks={t}
          toolbar={toolbar}
        />
      )}
      {toast && <div className="ap-toast">{toast}</div>}
      {settingsOpen && (
        <AnnotatorSettings
          cfg={annotator}
          onChange={(c) => { setAnnotator(c); apSaveAnnotator(c); }}
          onClose={() => setSettingsOpen(false)}
        />
      )}

      <TweaksPanel>
        <TweakSection label="Paper" />
        <TweakColor
          label="Palette" value={t.paperPalette}
          options={[
            ["#f6f1e6", "#2b2418", "#9a3b20"],
            ["#fbf8f1", "#231f17", "#8a5a18"],
            ["#f3f2ee", "#22241f", "#3f5a96"],
          ]}
          onChange={(v) => setTweak('paperPalette', v)}
        />
        <TweakSlider label="Body size" value={t.bodySize} min={16} max={23} unit="px" onChange={(v) => setTweak('bodySize', v)} />
        <TweakToggle label="Drop caps" value={t.dropCaps} onChange={(v) => setTweak('dropCaps', v)} />
        <TweakToggle label="Justified text" value={t.justify} onChange={(v) => setTweak('justify', v)} />
        <TweakSection label="Marginalia" />
        <TweakRadio label="Notes side" value={t.notesSide} options={['right', 'left']} onChange={(v) => setTweak('notesSide', v)} />
        <TweakToggle label="Retrieval scores" value={t.showScores} onChange={(v) => setTweak('showScores', v)} />
      </TweaksPanel>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
