import React from 'react';
import Upload from './screens/Upload';
import Pipeline from './screens/Pipeline';
import Reader from './screens/Reader';
import AnnotatorPanel, { apLoadAnnotator, apSaveAnnotator, AP_PROVIDERS } from './components/AnnotatorPanel';
import DisplayMenu, { PALETTES } from './components/DisplayMenu';
import Toast from './components/Toast';
import { startParse, jobEvents, annotateStream } from './lib/api';
import { hashPdf, exportSession, importSession } from './lib/session';
import FigurePopover from './components/FigurePopover';
import NotebookDrawer from './components/NotebookDrawer';

// Default annotation mode for fresh parses and legend-less notes.
const DEFAULT_MODE = 'history';

// ── Display / tweak defaults ──────────────────────────────────────────────────
// Palette [0] = Parchment — same hard-coded values that were previously in
// rootStyle, so the default visual output is unchanged.
const DISPLAY_DEFAULTS = {
  paperPalette: ['#f6f1e6', '#2b2418', '#9a3b20'],
  bodySize: 19,
  dropCaps: true,
  justify: true,
  showScores: true,
  notesSide: 'right',
  litCount: 3,
};

const DISPLAY_KEY = 'astroparse_display_v1';
const SESSION_KEY = 'astroparse_session_v2';

function loadDisplay() {
  try {
    const raw = localStorage.getItem(DISPLAY_KEY);
    if (raw) {
      const v = JSON.parse(raw);
      if (v && Array.isArray(v.paperPalette) && v.paperPalette.length === 3) return { ...DISPLAY_DEFAULTS, ...v };
    }
  } catch (e) { /* ignore corrupt */ }
  return { ...DISPLAY_DEFAULTS };
}

export default function App() {
  const [screen, setScreen] = React.useState('upload');
  const [fileName, setFileName] = React.useState('');
  const [stageEvents, setStageEvents] = React.useState([]);
  const [result, setResult] = React.useState(null);
  const [pdfHash, setPdfHash] = React.useState('');

  // noteModes: { [paraId]: modeId }
  const [noteModes, setNoteModes] = React.useState({});
  // commentary: { ['paraId:modeId']: { text, source, error, message, streaming? } }
  const [commentary, setCommentary] = React.useState({});
  const commentaryRef = React.useRef(commentary);
  commentaryRef.current = commentary;
  // generating: { ['paraId:modeId']: true }
  const [generating, setGenerating] = React.useState({});

  // Figure popover state: { fig, n, pos } or null
  const [figPop, setFigPop] = React.useState(null);

  // User notes state.
  const [userNotes, setUserNotes] = React.useState({});
  const [paperNote, setPaperNote] = React.useState('');
  const [notebookOpen, setNotebookOpen] = React.useState(false);

  // Toast state.
  const [toast, setToast] = React.useState(null);
  const toastTimerRef = React.useRef(null);

  // Annotator config — loaded from localStorage on mount.
  const [annotator, setAnnotator] = React.useState(apLoadAnnotator);
  const [settingsOpen, setSettingsOpen] = React.useState(false);

  // Display/tweak state — loaded from localStorage on mount.
  const [tweaks, setTweaks] = React.useState(loadDisplay);
  const [displayOpen, setDisplayOpen] = React.useState(false);

  // Inflight set — dedupes concurrent requests for the same (paraId, modeId).
  const inflight = React.useRef(new Set());

  // Toolbar file-import ref for session load.
  const importRef = React.useRef(null);

  // lastFileName for Upload screen (from localStorage session on mount).
  const [lastFileName, setLastFileName] = React.useState(null);
  // Stash for the raw session text so onOpenLast can restore it.
  const storedSessionRef = React.useRef(null);

  // showToast: sets toast message and auto-clears after 2600ms.
  const showToast = React.useCallback((msg) => {
    setToast(msg);
    if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
    toastTimerRef.current = setTimeout(() => setToast(null), 2600);
  }, []);

  const handleSaveNote = React.useCallback((paraId, text) => {
    setUserNotes((prev) => {
      if (!text.trim()) {
        const next = { ...prev }; delete next[paraId]; return next;
      }
      return { ...prev, [paraId]: { text, updatedAt: new Date().toISOString() } };
    });
  }, []);

  const handleSavePaperNote = React.useCallback((text) => {
    setPaperNote(text);
  }, []);

  // ── restoreSession ─────────────────────────────────────────────────────────
  // Shared restore path used by toolbar "load", upload-screen onSession, and
  // onOpenLast.  Reads text, parses, populates all state, switches to reader.
  const restoreSession = React.useCallback((text) => {
    try {
      const s = importSession(text);
      fromParseRef.current = false;
      setFileName(s.fileName || '');
      setResult({
        paper: s.paper,
        litPapers: s.litPapers,
        litByPara: s.litByPara,
        figures: s.figures || [],
        litByFig: s.litByFig || {},
        equations: s.equations || [],
        references: s.references || [],
        citeIndex: s.citeIndex || {},
      });
      setPdfHash(s.pdfHash || '');
      if (s.noteModes) {
        setNoteModes(s.noteModes);
      } else {
        const modes = {};
        (s.paper?.paragraphs || []).forEach((p) => { modes[p.id] = DEFAULT_MODE; });
        setNoteModes(modes);
      }
      setCommentary(s.commentary || {});
      setGenerating({});
      setUserNotes(s.userNotes || {});
      setPaperNote(s.paperNote || '');
      setScreen('reader');
      showToast('session restored');
    } catch {
      showToast('could not read that session file');
    }
  }, [showToast]);

  // ── On mount: read localStorage session for Upload "open last" button ──────
  React.useEffect(() => {
    try {
      const raw = localStorage.getItem(SESSION_KEY);
      if (raw) {
        const parsed = JSON.parse(raw);
        if (parsed && parsed.fileName) {
          storedSessionRef.current = raw;
          setLastFileName(parsed.fileName);
        }
      }
    } catch (e) { /* ignore corrupt */ }
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  // ── Autosave ───────────────────────────────────────────────────────────────
  // Write to localStorage whenever commentary or noteModes change, if in reader.
  React.useEffect(() => {
    if (screen !== 'reader' || !result) return;
    try {
      const json = exportSession({
        fileName,
        pdfHash,
        paper: result.paper,
        litPapers: result.litPapers,
        litByPara: result.litByPara,
        figures: result.figures || [],
        litByFig: result.litByFig || {},
        equations: result.equations || [],
        references: result.references || [],
        citeIndex: result.citeIndex || {},
        userNotes,
        paperNote,
        noteModes,
        commentary,
      });
      localStorage.setItem(SESSION_KEY, json);
    } catch (e) { /* ignore quota errors */ }
  }, [commentary, noteModes, userNotes, paperNote]); // eslint-disable-line react-hooks/exhaustive-deps

  // ── Persist display preferences ───────────────────────────────────────────
  React.useEffect(() => {
    try { localStorage.setItem(DISPLAY_KEY, JSON.stringify(tweaks)); } catch (e) { /* ignore */ }
  }, [tweaks]);

  // ── CSS custom properties from palette ────────────────────────────────────
  const pal = tweaks.paperPalette || DISPLAY_DEFAULTS.paperPalette;
  const rootStyle = {
    '--paper': pal[0],
    '--ink': pal[1],
    '--rubric': pal[2],
    '--body-size': tweaks.bodySize + 'px',
  };

  // generate: stream an annotation for (targetId, modeId) via the backend.
  // targetId may be a paragraph id OR a figure id — resolved below.
  const generate = React.useCallback(async (targetId, modeId) => {
    const k = `${targetId}:${modeId}`;
    if (inflight.current.has(k)) return;
    if (!result) return;
    inflight.current.add(k);
    setGenerating((g) => ({ ...g, [k]: true }));
    // Resolve target: paragraph first, then figure
    const para = result.paper.paragraphs.find((p) => p.id === targetId);
    let paragraphText, section, litIds;
    if (para) {
      paragraphText = para.text;
      section = para.section;
      litIds = result.litByPara[targetId] || [];
    } else {
      const fig = (result.figures || []).find((f) => f.id === targetId);
      if (!fig) {
        inflight.current.delete(k);
        setGenerating((g) => { const n = { ...g }; delete n[k]; return n; });
        return;
      }
      paragraphText = fig.caption;
      section = fig.label;
      litIds = (result.litByFig || {})[targetId] || [];
    }
    const slicedLitIds = litIds.slice(0, tweaks.litCount);
    const lit = slicedLitIds.map((id) => result.litPapers[id]).filter(Boolean);
    const paraId = targetId;

    let paperContext = null;
    if (modeId === 'referee' && para) {
      const paras = result.paper.paragraphs;
      const idx = paras.findIndex((p) => p.id === targetId);
      paperContext = {
        title: result.paper.title,
        sectionOutline: [...new Set(paras.map((p) => p.section))],
        opening: paras[0]?.text?.slice(0, 400) || '',
        prevParagraph: idx > 0 ? paras[idx - 1].text.slice(0, 300) : '',
        nextParagraph: idx < paras.length - 1 ? paras[idx + 1].text.slice(0, 300) : '',
      };
    }

    try {
      setCommentary((c) => ({ ...c, [k]: { text: '', cites: slicedLitIds, source: 'llm', streaming: true } }));
      for await (const ev of annotateStream({
        paragraphId: paraId,
        paragraph: paragraphText,
        section: section,
        mode: modeId,
        lit: lit.map((l) => ({
          id: l.id,
          short: l.short,
          title: l.title,
          journal: l.journal || 'arXiv',
          abstract: l.abstract,
        })),
        provider: annotator.provider,
        model: annotator.model,
        key: annotator.key,
        paperContext,
      })) {
        if (ev.event === 'token') {
          setCommentary((c) => ({ ...c, [k]: { ...c[k], text: (c[k]?.text || '') + ev.data.text } }));
        } else if (ev.event === 'done') {
          setCommentary((c) => ({ ...c, [k]: { text: ev.data.text, cites: slicedLitIds, source: 'llm' } }));
        } else if (ev.event === 'error') {
          setCommentary((c) => ({ ...c, [k]: { text: '', cites: [], source: 'llm', error: true, message: ev.data.message } }));
        }
      }
    } catch (e) {
      const msg = String(e.message || e).slice(0, 200);
      setCommentary((c) => ({ ...c, [k]: { text: '', cites: [], source: 'llm', error: true, message: msg } }));
      if (e instanceof TypeError || (e.message && e.message.startsWith('Failed to fetch'))) {
        showToast('could not reach the annotator backend');
      }
    } finally {
      inflight.current.delete(k);
      setGenerating((g) => { const n = { ...g }; delete n[k]; return n; });
    }
  }, [result, annotator, tweaks, showToast]);

  // Auto-generate on fresh parse (not session import).
  const fromParseRef = React.useRef(false);
  const prevResultRef = React.useRef(null);

  React.useEffect(() => {
    if (!result || result === prevResultRef.current) return;
    prevResultRef.current = result;
    if (!fromParseRef.current) return;
    result.paper.paragraphs.forEach((p) => {
      generate(p.id, DEFAULT_MODE);
    });
  }, [result, generate]);

  async function onPdf(file) {
    setFileName(file.name);
    setScreen('pipeline');
    setStageEvents([]);
    setResult(null);
    setNoteModes({});
    setCommentary({});
    setGenerating({});
    fromParseRef.current = true;
    try {
      setPdfHash(await hashPdf(await file.arrayBuffer()));
      const jobId = await startParse(file);
      for await (const ev of jobEvents(jobId)) {
        if (ev.event === 'stage') {
          setStageEvents((s) => [...s, ev.data]);
        } else if (ev.event === 'result') {
          const data = ev.data;
          // Prefer backend pdfHash (authoritative); fall back to frontend-computed hash.
          if (data.pdfHash) setPdfHash(data.pdfHash);
          setResult(data);
          const modes = {};
          (data.paper?.paragraphs || []).forEach((p) => { modes[p.id] = DEFAULT_MODE; });
          setNoteModes(modes);
          setCommentary({});
          setGenerating({});
          setScreen('reader');
        } else if (ev.event === 'error') {
          setStageEvents((s) => {
            const failedIndex = s.length ? Math.max(...s.map((x) => x.index)) : 0;
            return [...s, { index: failedIndex, name: '', status: 'failed', log: ev.data.message }];
          });
        }
      }
    } catch (e) {
      setStageEvents((s) => [
        ...s,
        { index: 0, name: '', status: 'failed', log: String(e.message || e) },
      ]);
    }
  }

  function onSetMode(paraId, modeId) {
    setNoteModes((m) => (m[paraId] === modeId ? m : { ...m, [paraId]: modeId }));
    if (!commentaryRef.current[`${paraId}:${modeId}`]) generate(paraId, modeId);
  }

  function onRegenerate(paraId, modeId) {
    generate(paraId, modeId);
  }

  function onSetAllModes(modeId) {
    if (!result?.paper?.paragraphs) return;
    const next = {};
    result.paper.paragraphs.forEach((p) => { next[p.id] = modeId; });
    setNoteModes(next);
    result.paper.paragraphs.forEach((p) => {
      if (!commentaryRef.current[`${p.id}:${modeId}`]) generate(p.id, modeId);
    });
  }

  function onReset() {
    setScreen('upload');
    setResult(null);
    setNoteModes({});
    setCommentary({});
    setGenerating({});
    setFileName('');
    fromParseRef.current = false;
  }

  // ── Save session ───────────────────────────────────────────────────────────
  function onSaveSession() {
    if (!result) return;
    const json = exportSession({
      fileName,
      pdfHash,
      paper: result.paper,
      litPapers: result.litPapers,
      litByPara: result.litByPara,
      figures: result.figures || [],
      litByFig: result.litByFig || {},
      equations: result.equations || [],
      references: result.references || [],
      citeIndex: result.citeIndex || {},
      userNotes,
      paperNote,
      noteModes,
      commentary,
    });
    const blob = new Blob([json], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = fileName.replace(/\.pdf$/i, '') + '.beacon.json';
    a.click();
    URL.revokeObjectURL(url);
    showToast('session written to ‹' + a.download + '›');
  }

  // onOpenLast: restores the full session stored in localStorage on mount.
  function onOpenLast() {
    if (storedSessionRef.current) {
      restoreSession(storedSessionRef.current);
    }
  }

  // Toolbar rendered inside Reader via toolbar prop.
  const toolbar = (
    <div className="ap-toolbar">
      <span className="ap-toolbar-mark">beacon</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() || annotator.provider}
      </button>
      <button className="ap-textbtn" onClick={() => setDisplayOpen(true)}>display</button>
      <button className="ap-toolbar-btn" onClick={() => setNotebookOpen(true)} title="open notebook">&#x1F4D3;</button>
      <button className="ap-textbtn" onClick={onSaveSession}>
        save session &darr;
      </button>
      <button
        className="ap-textbtn"
        onClick={() => importRef.current && importRef.current.click()}
      >
        load
      </button>
      <button className="ap-textbtn" onClick={onReset}>new manuscript</button>
      <input
        ref={importRef}
        type="file"
        accept=".json,application/json"
        style={{ display: 'none' }}
        onChange={(e) => {
          const f = e.target.files[0]; if (!f) return;
          const r = new FileReader();
          r.onload = () => restoreSession(r.result);
          r.readAsText(f);
          e.target.value = '';
        }}
      />
    </div>
  );

  return (
    <div className="ap-root" style={rootStyle}>
      {screen === 'upload' && (
        <Upload
          onPdf={onPdf}
          lastFileName={lastFileName}
          onOpenLast={onOpenLast}
          onSession={restoreSession}
        />
      )}
      {screen === 'pipeline' && (
        <Pipeline
          fileName={fileName}
          events={stageEvents}
          onSkip={result ? () => setScreen('reader') : null}
          onReset={() => setScreen('upload')}
        />
      )}
      {screen === 'reader' && result && (
        <Reader
          paper={result.paper}
          litPapers={result.litPapers || {}}
          litByPara={result.litByPara || {}}
          figures={result.figures || []}
          pdfHash={pdfHash}
          equations={result.equations || []}
          commentary={commentary}
          noteModes={noteModes}
          generating={generating}
          onSetMode={onSetMode}
          onRegenerate={onRegenerate}
          onSetAllModes={onSetAllModes}
          onOpenFigure={(n, e) => {
            const figures = result.figures || [];
            const fig = figures.find((f) => {
              const labelNum = f.label.replace(/^(Figure|Fig\.?)\s*/i, '').trim();
              return labelNum === n;
            });
            if (!fig) return;
            const r = e.currentTarget.getBoundingClientRect();
            setFigPop({ fig, n, pos: { x: r.left, y: r.bottom } });
          }}
          tweaks={tweaks}
          toolbar={toolbar}
          citeIndex={result?.citeIndex || {}}
          references={result?.references || []}
          userNotes={userNotes}
          onSaveNote={handleSaveNote}
        />
      )}
      <Toast msg={toast} />
      {settingsOpen && (
        <AnnotatorPanel
          cfg={annotator}
          onChange={(c) => { setAnnotator(c); apSaveAnnotator(c); }}
          onClose={() => setSettingsOpen(false)}
        />
      )}
      {displayOpen && (
        <DisplayMenu
          tweaks={tweaks}
          onChange={(t) => setTweaks(t)}
          onClose={() => setDisplayOpen(false)}
        />
      )}
      {figPop && (
        <FigurePopover
          fig={figPop.fig}
          n={figPop.n}
          pdfHash={pdfHash}
          pos={figPop.pos}
          commentary={commentary}
          generating={generating}
          onGenerate={generate}
          onClose={() => setFigPop(null)}
        />
      )}
      <NotebookDrawer
        open={notebookOpen}
        onClose={() => setNotebookOpen(false)}
        paper={result?.paper}
        userNotes={userNotes}
        paperNote={paperNote}
        onSavePaperNote={handleSavePaperNote}
        onScrollToPara={(paraId) => {
          document.querySelector(`[data-comment-anchor="para-${paraId}"]`)
            ?.scrollIntoView({ behavior: 'smooth', block: 'center' });
          setNotebookOpen(false);
        }}
      />
    </div>
  );
}
