// app.jsx — Main shell for PM Writing Agent.

const { useState: useStateA, useEffect: useEffectA, useRef: useRefA, useCallback: useCallbackA, useMemo: useMemoA } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": "#b85c38",
  "paper": "white",
  "density": "comfortable",
  "docFont": "newsreader",
  "chatWidth": "regular"
}/*EDITMODE-END*/;

const ACCENT_PRESETS = {
  "#b85c38": { soft: "rgba(184, 92, 56, 0.10)", rim: "rgba(184, 92, 56, 0.28)" }, // terracotta
  "#6b7a3a": { soft: "rgba(107, 122, 58, 0.10)", rim: "rgba(107, 122, 58, 0.28)" }, // moss
  "#7c4a7a": { soft: "rgba(124, 74, 122, 0.10)", rim: "rgba(124, 74, 122, 0.30)" }, // plum
  "#1f1b16": { soft: "rgba(31, 27, 22, 0.08)",  rim: "rgba(31, 27, 22, 0.25)" },    // mono ink
  // Apple system colors
  "#007AFF": { soft: "rgba(0, 122, 255, 0.10)",  rim: "rgba(0, 122, 255, 0.30)" },   // Apple Blue
  "#5856D6": { soft: "rgba(88, 86, 214, 0.10)",  rim: "rgba(88, 86, 214, 0.30)" },   // Apple Indigo
  "#FF2D55": { soft: "rgba(255, 45, 85, 0.08)",  rim: "rgba(255, 45, 85, 0.28)" },   // Apple Pink
  "#34C759": { soft: "rgba(52, 199, 89, 0.10)",  rim: "rgba(52, 199, 89, 0.30)" },   // Apple Green
  "#FF9500": { soft: "rgba(255, 149, 0, 0.10)",  rim: "rgba(255, 149, 0, 0.30)" },   // Apple Orange
};

const PAPER_PRESETS = {
  warm:    { paper: "#faf7f0", deep: "#f3eee2", rim: "#ece5d3", rule: "#e7e0cf", ruleStrong: "#d6cdb7" },
  neutral: { paper: "#f8f7f4", deep: "#efede7", rim: "#e6e3d9", rule: "#e3dfd4", ruleStrong: "#cfc9b9" },
  cool:    { paper: "#f6f6f2", deep: "#ecece5", rim: "#e2e2d9", rule: "#dfded4", ruleStrong: "#c9c9bb" },
  bright:  { paper: "#ffffff", deep: "#f5f3ed", rim: "#ebe8df", rule: "#e9e6dd", ruleStrong: "#d4d0c2" },
  white:   { paper: "#ffffff", deep: "#f6f6f7", rim: "#ececef", rule: "#e6e6e9", ruleStrong: "#d0d0d4" },
};

const DENSITY_PRESETS = {
  cozy:        { padY: 80, padX: 96, size: 19, lead: 1.78, measure: "64ch" },
  comfortable: { padY: 64, padX: 80, size: 18, lead: 1.7,  measure: "68ch" },
  compact:     { padY: 44, padX: 60, size: 16, lead: 1.55, measure: "76ch" },
};

const DOC_FONTS = {
  newsreader:    '"Newsreader", "Source Serif Pro", Georgia, serif',
  charter:       '"Source Serif 4", Charter, "Iowan Old Style", Georgia, serif',
  instrument:    '"Instrument Serif", Georgia, serif',
  sans:          '"Instrument Sans", ui-sans-serif, system-ui, sans-serif',
  mono:          '"JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace',
};

const CHAT_WIDTHS = {
  narrow:  '320px',
  regular: '360px',
  wide:    '420px',
};

// ─── History stack helpers ─────────────────────────────────
function makeHistory(initial = '') {
  return { past: [], present: initial, future: [] };
}
function pushHistory(h, next) {
  if (next === h.present) return h;
  return { past: [...h.past, h.present], present: next, future: [] };
}
function undoHistory(h) {
  if (h.past.length === 0) return h;
  const past = h.past.slice();
  const prev = past.pop();
  return { past, present: prev, future: [h.present, ...h.future] };
}
function redoHistory(h) {
  if (h.future.length === 0) return h;
  const future = h.future.slice();
  const next = future.shift();
  return { past: [...h.past, h.present], present: next, future };
}

// ─── Streaming helpers ─────────────────────────────────────
async function streamText(targetText, onChunk, opts = {}) {
  const chunkSize = opts.chunkSize || 18;
  const delayMs   = opts.delayMs   || 8;
  let i = 0;
  while (i < targetText.length) {
    i = Math.min(targetText.length, i + chunkSize + Math.floor(Math.random() * 10));
    onChunk(targetText.slice(0, i));
    await new Promise(r => setTimeout(r, delayMs));
  }
}

// ─── Robust section extraction (delimiter-based, no JSON pitfalls) ──
// Tolerates truncated responses (missing closing tags).
function extractSections(text) {
  if (!text) return null;
  const t = text;

  // Try to grab between open and close tags; if close is missing, grab to end-of-text.
  const grab = (tag) => {
    const open = new RegExp(`<${tag}>`, 'i');
    const close = new RegExp(`</${tag}>`, 'i');
    const om = t.match(open);
    if (!om) return null;
    const startIdx = om.index + om[0].length;
    const rest = t.slice(startIdx);
    const cm = rest.match(close);
    const body = cm ? rest.slice(0, cm.index) : rest;
    return body;
  };

  let chat = grab('chat');
  let title = grab('title');
  let document = grab('document');
  if (document == null) return null;

  // If document was truncated mid-stream, strip any partial tag at the end.
  document = document.replace(/<\/?[a-z]*$/i, '');
  document = document.replace(/^\n/, '').replace(/\n+$/, '');

  return {
    chat: (chat || '').trim(),
    title: (title || '').trim(),
    document,
    truncated: !t.match(/<\/document>/i),
  };
}

// ─── Build the prompt to Claude ───────────────────────────
function buildPrompt({ history, userMsg, quote, files, currentDoc, currentTitle }) {
  const docState = currentDoc && currentDoc.trim()
    ? `Current document title: "${currentTitle}"\n\nCurrent document body (markdown):\n<document>\n${currentDoc}\n</document>`
    : `No document exists yet. Create one.`;

  const quoteBlock = quote
    ? `\nThe user has quoted this section of the document — it's the focus of their request:\n<quoted-section>\n${quote}\n</quoted-section>\n`
    : '';

  let filesBlock = '';
  if (files && files.length) {
    const parts = files.map((f) => {
      const meta = `name="${f.name}" type="${f.kind}" size="${f.size || 0}"`;
      if (f.kind === 'text' || f.kind === 'docx') {
        const trim = f.truncated ? '\n\n[content truncated — file exceeds context limit]' : '';
        return `<file ${meta}>\n${f.content || ''}${trim}\n</file>`;
      }
      if (f.kind === 'image') {
        return `<file ${meta}>\n[Image file — content not readable in this text-only context. The user has provided this as reference.]\n</file>`;
      }
      if (f.kind === 'pdf') {
        return `<file ${meta}>\n[PDF file — content not extractable in this prototype. Ask the user to paste relevant text, or attach a DOCX/MD version.]\n</file>`;
      }
      return `<file ${meta}>\n[File attached but content not readable.]\n</file>`;
    });
    filesBlock = `\nThe user has attached the following files. Treat their content as primary reference material for this request:\n${parts.join('\n\n')}\n`;
  }

  const recent = history.slice(-6).map(m => {
    const who = m.role === 'user' ? 'User' : 'You (assistant)';
    const q   = m.quote ? `\n[quoted: "${m.quote.slice(0, 120)}…"]` : '';
    const fs  = (m.files && m.files.length) ? `\n[attached: ${m.files.map(x => x.name).join(', ')}]` : '';
    return `${who}:${q}${fs}\n${m.content}`;
  }).join('\n\n');

  return `You are a senior product manager's writing partner. The user is drafting a PM document (PRDs, strategy docs, launch comms, RFCs, meeting notes, roadmaps).

${docState}
${quoteBlock}${filesBlock}
${recent ? `\nRecent conversation:\n${recent}\n` : ''}
The user's latest message:
"${userMsg}"

Your job:
1) Reply briefly in chat (1–3 sentences, conversational, no headers, no bullet lists).
2) Output the COMPLETE updated markdown document — full body, not a diff.
3) Output a short title for the document.

Writing style for the document:
- Strong, declarative prose. Clear hierarchy. # Title, ## Section, ### Subsection.
- PM-grade specificity: concrete metrics, dates, examples where appropriate.
- Use lists, tables, blockquotes where they help — but don't overdo it.
- Don't pad with filler. Better short and sharp than long and vague.
- CRITICAL: keep the entire document under 600 words / 4000 characters. Be dense, not verbose. The response budget is tight — a complete short document is far better than a half-finished long one. If the user wants more detail, they'll ask.
- If the user quoted a section, focus changes there but return the whole document with that section revised.
- If the user only chitchats (e.g. "hi"), keep the document as-is.

Respond using EXACTLY these XML-style tags, with nothing before <chat> and nothing after </document>:

<chat>
your brief conversational reply here
</chat>
<title>
Short Document Title
</title>
<document>
# The full markdown document goes here

Write it as plain markdown with real newlines. No escaping needed.
Do not wrap this in code fences.
</document>`;
}

// ─── Apply theme tokens to root ───────────────────────────
function applyTheme(t) {
  const root = document.documentElement;
  const accent = ACCENT_PRESETS[t.accent] || ACCENT_PRESETS["#b85c38"];
  root.style.setProperty('--accent', t.accent);
  root.style.setProperty('--accent-soft', accent.soft);
  root.style.setProperty('--accent-rim', accent.rim);

  const paper = PAPER_PRESETS[t.paper] || PAPER_PRESETS.warm;
  root.style.setProperty('--paper', paper.paper);
  root.style.setProperty('--paper-deep', paper.deep);
  root.style.setProperty('--paper-rim', paper.rim);
  root.style.setProperty('--rule', paper.rule);
  root.style.setProperty('--rule-strong', paper.ruleStrong);

  const d = DENSITY_PRESETS[t.density] || DENSITY_PRESETS.comfortable;
  root.style.setProperty('--doc-pad-y', d.padY + 'px');
  root.style.setProperty('--doc-pad-x', d.padX + 'px');
  root.style.setProperty('--doc-size', d.size + 'px');
  root.style.setProperty('--doc-leading', String(d.lead));
  root.style.setProperty('--doc-measure', d.measure);

  root.style.setProperty('--doc-font', DOC_FONTS[t.docFont] || DOC_FONTS.newsreader);
  root.style.setProperty('--chat-w', CHAT_WIDTHS[t.chatWidth] || CHAT_WIDTHS.regular);
}

// ─── Main App ─────────────────────────────────────────────
function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  useEffectA(() => { applyTheme(t); }, [t]);

  // Messages
  const [messages, setMessages] = useStateA([]);
  // Document state w/ history
  const [history, setHistory] = useStateA(makeHistory(''));
  const markdown = history.present;
  const [title, setTitle] = useStateA('Untitled');
  const [mode, setMode] = useStateA('preview'); // preview | edit | split
  const [isStreaming, setIsStreaming] = useStateA(false);
  const [pendingQuote, setPendingQuote] = useStateA(null);
  const [selection, setSelection] = useStateA(null);
  const [versions, setVersions] = useStateA([]); // [{id, label, content, ts, source}]
  const [showHistory, setShowHistory] = useStateA(false);
  const [toast, setToast] = useStateA(null);
  const [pendingFiles, setPendingFiles] = useStateA([]);

  // For the streaming target the user has typed concurrently — we capture
  // pre-stream content so manual edits during the stream don't tangle.
  const streamingRef = useRefA({ active: false });

  // ─── Document mutations ────────────────────────────────
  const setMarkdown = useCallbackA((next) => {
    setHistory(h => pushHistory(h, next));
  }, []);

  const replaceMarkdown = useCallbackA((next) => {
    // Used during streaming — replace present but don't keep every intermediate
    setHistory(h => ({ ...h, present: next }));
  }, []);

  const commitToHistory = useCallbackA((finalContent) => {
    setHistory(h => {
      // If the previous "past" top already equals finalContent, don't double-add.
      if (h.past.length && h.past[h.past.length - 1] === finalContent) return h;
      // We need a clean push: the present (during stream) was being replaced;
      // make sure the "before" content is preserved in past.
      return { past: [...h.past], present: finalContent, future: [] };
    });
  }, []);

  // ─── Toast helper ─────────────────────────────────────
  const showToast = useCallbackA((msg) => {
    setToast(msg);
    clearTimeout(showToast._t);
    showToast._t = setTimeout(() => setToast(null), 2200);
  }, []);

  // ─── Send to Claude ────────────────────────────────────
  const onSend = useCallbackA(async (userText, quote, files) => {
    const userMsg = {
      id: 'u-' + Date.now(),
      role: 'user',
      content: userText,
      quote: quote,
      files: files || null,
    };
    const placeholderId = 'a-' + Date.now();
    setMessages(prev => [
      ...prev,
      userMsg,
      { id: placeholderId, role: 'assistant', content: '', thinking: true },
    ]);

    setIsStreaming(true);
    streamingRef.current.active = true;
    const preStreamContent = markdown;
    const preStreamTitle = title;

    try {
      const prompt = buildPrompt({
        history: messages,
        userMsg: userText,
        quote,
        files,
        currentDoc: markdown,
        currentTitle: title,
      });
      const raw = await window.claude.complete(prompt);
      const parsed = extractSections(raw);

      if (!parsed || typeof parsed.document !== 'string') {
        // Fallback: just show raw as chat
        setMessages(prev => prev.map(m => m.id === placeholderId
          ? { ...m, thinking: false, content: (raw || "I couldn't generate that. Try again?").trim().slice(0, 800) }
          : m
        ));
        setIsStreaming(false);
        streamingRef.current.active = false;
        return;
      }

      const chatReply = (parsed.chat || '').trim();
      const docTarget = String(parsed.document || '');
      const newTitle  = (parsed.title || title || 'Untitled').trim();
      const wasTruncated = !!parsed.truncated;

      // 1) Stream the chat reply
      let chatSoFar = '';
      const chatTokens = chatReply.split(/(\s+)/);
      for (const tok of chatTokens) {
        chatSoFar += tok;
        setMessages(prev => prev.map(m => m.id === placeholderId
          ? { ...m, thinking: false, content: chatSoFar }
          : m
        ));
        await new Promise(r => setTimeout(r, 12));
      }

      if (wasTruncated) {
        const tail = (chatSoFar ? '\n\n' : '') + '_(Response was cut short — ask me to "continue" or to shorten the document.)_';
        setMessages(prev => prev.map(m => m.id === placeholderId
          ? { ...m, content: (m.content || '') + tail }
          : m
        ));
      }

      // 2) Stream the document content (replace present continuously)
      const docChanged = docTarget !== preStreamContent;
      if (docChanged) {
        // Reveal-by-chunk: replace .present without pushing history each step
        await streamText(docTarget, (slice) => {
          replaceMarkdown(slice);
        }, { chunkSize: 22, delayMs: 6 });

        // Commit: push preStreamContent into past, set present=docTarget
        setHistory(h => {
          const newPast = preStreamContent !== '' || h.past.length > 0
            ? [...h.past, preStreamContent]
            : h.past;
          return { past: newPast, present: docTarget, future: [] };
        });

        // Snapshot as a version
        const diff = window.diffLines(preStreamContent, docTarget);
        const label = preStreamContent.trim() === ''
          ? `Created · ${newTitle}`
          : `Edit · ${userText.slice(0, 36)}${userText.length > 36 ? '…' : ''}`;
        setVersions(prev => [...prev, {
          id: 'v-' + Date.now(),
          label,
          content: docTarget,
          ts: Date.now(),
          source: 'agent',
        }]);

        // Update title if changed
        if (newTitle && newTitle !== preStreamTitle) {
          setTitle(newTitle);
        }

        // Append a doc-action card to the assistant message
        setMessages(prev => prev.map(m => m.id === placeholderId
          ? { ...m, docAction: {
              kind: preStreamContent.trim() === '' ? 'created' : (quote ? 'rewrote' : 'updated'),
              title: newTitle,
              added: diff.added,
              removed: diff.removed,
            } }
          : m
        ));
      }
    } catch (err) {
      console.error(err);
      setMessages(prev => prev.map(m => m.id === placeholderId
        ? { ...m, thinking: false, content: "Something went wrong. " + (err.message || '') }
        : m
      ));
    } finally {
      setIsStreaming(false);
      streamingRef.current.active = false;
    }
  }, [messages, markdown, title, replaceMarkdown]);

  // ─── Undo / Redo ───────────────────────────────────────
  const canUndo = history.past.length > 0 && !isStreaming;
  const canRedo = history.future.length > 0 && !isStreaming;
  const onUndo = useCallbackA(() => setHistory(h => undoHistory(h)), []);
  const onRedo = useCallbackA(() => setHistory(h => redoHistory(h)), []);

  // Keyboard shortcuts
  useEffectA(() => {
    function onKey(e) {
      const mod = e.metaKey || e.ctrlKey;
      if (mod && e.key.toLowerCase() === 'z' && !e.shiftKey) {
        // Don't hijack textareas' native undo if the user is in one
        const tag = (e.target.tagName || '').toLowerCase();
        if (tag === 'textarea' || tag === 'input') return;
        e.preventDefault();
        if (canUndo) onUndo();
      } else if (mod && (e.key.toLowerCase() === 'z' && e.shiftKey || e.key.toLowerCase() === 'y')) {
        const tag = (e.target.tagName || '').toLowerCase();
        if (tag === 'textarea' || tag === 'input') return;
        e.preventDefault();
        if (canRedo) onRedo();
      } else if (mod && e.key.toLowerCase() === 's') {
        e.preventDefault();
        // Manual snapshot
        if (markdown.trim()) {
          setVersions(prev => [...prev, {
            id: 'v-' + Date.now(),
            label: 'Manual snapshot',
            content: markdown,
            ts: Date.now(),
            source: 'user',
          }]);
          showToast('Snapshot saved');
        }
      }
    }
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [canUndo, canRedo, onUndo, onRedo, markdown, showToast]);

  // ─── Selection handling ───────────────────────────────
  const onSelectionChange = useCallbackA((s) => {
    setSelection(s);
  }, []);

  const onQuoteSelection = useCallbackA(() => {
    if (!selection) return;
    setPendingQuote(selection.text);
    setSelection(null);
    // Clear browser selection
    window.getSelection()?.removeAllRanges();
    showToast('Added to chat as quote');
  }, [selection, showToast]);

  const onRewriteSelection = useCallbackA(() => {
    if (!selection) return;
    const txt = selection.text;
    setPendingQuote(txt);
    setSelection(null);
    window.getSelection()?.removeAllRanges();
    // Auto-send "Rewrite this section."
    setTimeout(() => onSend('Please rewrite this section to be clearer and more compelling.', txt), 80);
  }, [selection, onSend]);

  // ─── New chat / reset ────────────────────────────────
  const onNewChat = useCallbackA(() => {
    if (markdown.trim() && !confirm('Start a new document? Current draft will be cleared (you can find it in version history).')) return;
    if (markdown.trim()) {
      setVersions(prev => [...prev, {
        id: 'v-' + Date.now(),
        label: 'Before new chat · ' + title,
        content: markdown,
        ts: Date.now(),
        source: 'user',
      }]);
    }
    setMessages([]);
    setHistory(makeHistory(''));
    setTitle('Untitled');
    setPendingQuote(null);
    setSelection(null);
  }, [markdown, title]);

  // ─── Restore version ─────────────────────────────────
  const onRestore = useCallbackA((content, label) => {
    setHistory(h => pushHistory(h, content));
    showToast('Restored: ' + label);
  }, [showToast]);

  // ─── Export ──────────────────────────────────────────
  const onExport = useCallbackA(() => {
    const blob = new Blob([markdown], { type: 'text/markdown' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = (title || 'document').replace(/[^\w\- ]+/g, '').replace(/\s+/g, '-').toLowerCase() + '.md';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }, [markdown, title]);

  // ─── Render ──────────────────────────────────────────
  return (
    <React.Fragment>
      <div className="app">
        <Chat
          messages={messages}
          pendingQuote={pendingQuote}
          setPendingQuote={setPendingQuote}
          pendingFiles={pendingFiles}
          setPendingFiles={setPendingFiles}
          isStreaming={isStreaming}
          onSend={onSend}
          onNewChat={onNewChat}
          onJumpToDoc={() => setMode('preview')}
          docExists={!!markdown.trim()}
        />
        <DocumentPane
          title={title}
          setTitle={setTitle}
          markdown={markdown}
          setMarkdown={setMarkdown}
          mode={mode}
          setMode={setMode}
          isStreaming={isStreaming}
          canUndo={canUndo}
          canRedo={canRedo}
          onUndo={onUndo}
          onRedo={onRedo}
          onOpenHistory={() => setShowHistory(true)}
          onExport={onExport}
          versionCount={versions.length}
          onSelectionChange={onSelectionChange}
        />
      </div>

      <SelectionToolbar
        selection={selection}
        onQuote={onQuoteSelection}
        onRewrite={onRewriteSelection}
      />

      {showHistory ? (
        <VersionsModal
          versions={versions}
          currentMarkdown={markdown}
          onClose={() => setShowHistory(false)}
          onRestore={onRestore}
        />
      ) : null}

      {toast ? <div className="toast">{toast}</div> : null}

      <TweaksPanel>
        <TweakSection label="Theme" />
        <TweakColor
          label="Warm accent"
          value={t.accent}
          options={["#b85c38", "#6b7a3a", "#7c4a7a", "#1f1b16"]}
          onChange={(v) => setTweak('accent', v)}
        />
        <TweakColor
          label="Apple accent"
          value={t.accent}
          options={["#007AFF", "#5856D6", "#FF2D55", "#34C759", "#FF9500"]}
          onChange={(v) => setTweak('accent', v)}
        />
        <TweakRadio
          label="Paper"
          value={t.paper}
          options={["warm", "neutral", "cool", "bright", "white"]}
          onChange={(v) => setTweak('paper', v)}
        />
        <TweakSection label="Document" />
        <TweakSelect
          label="Document font"
          value={t.docFont}
          options={[
            { value: "newsreader", label: "Newsreader (serif)" },
            { value: "charter", label: "Source Serif" },
            { value: "instrument", label: "Instrument Serif" },
            { value: "sans", label: "Instrument Sans" },
            { value: "mono", label: "JetBrains Mono" },
          ]}
          onChange={(v) => setTweak('docFont', v)}
        />
        <TweakRadio
          label="Density"
          value={t.density}
          options={["compact", "comfortable", "cozy"]}
          onChange={(v) => setTweak('density', v)}
        />
        <TweakSection label="Layout" />
        <TweakRadio
          label="Chat width"
          value={t.chatWidth}
          options={["narrow", "regular", "wide"]}
          onChange={(v) => setTweak('chatWidth', v)}
        />
      </TweaksPanel>
    </React.Fragment>
  );
}

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