// Savvie · screens
// Sidebar, Inbox, EmptyState, SaveModal, ItemDetail, Digest, Toast

const { useState, useEffect } = React;

// =================== SIDEBAR =================================
function Sidebar({ view, setView, counts, items, extraCategories = [], user, onAuthClick, onSignOut }) {
  const navItems = [
    { id: 'inbox',   label: 'saved',    count: counts.total, glyph: '▤' },
    { id: 'digest',  label: 'digest',   count: counts.stale, glyph: '◔' },
  ];
  const { CATEGORIES } = window.SAVVIE_DATA;
  const itemsByCat = {};
  (items || []).forEach((it) => {
    itemsByCat[it.category] = (itemsByCat[it.category] || 0) + 1;
  });
  const builtIn = Object.values(CATEGORIES)
    .sort((a, b) => a.order - b.order)
    .map(c => ({ id: c.id, label: c.label, glyph: c.emoji, count: itemsByCat[c.id] || 0 }));
  const userMade = extraCategories
    .map(c => ({ id: c.id, label: c.label, glyph: c.emoji, count: itemsByCat[c.id] || 0 }));
  return (
    <aside className="sv-sidebar" data-screen-label="sidebar">
      <div onClick={() => setView('welcome')} style={{ cursor: 'pointer' }}>
        <Logo />
      </div>
      <nav className="sv-nav">
        <div className="sv-nav-section">main</div>
        {navItems.map(n => (
          <button
            key={n.id}
            className={`sv-nav-item ${view === n.id ? 'active' : ''}`}
            onClick={() => setView(n.id)}
          >
            <span style={{ width: 14, display: 'inline-block', textAlign: 'center', opacity: 0.7 }}>{n.glyph}</span>
            <span>{n.label}</span>
            <span className="count">{n.count}</span>
          </button>
        ))}
        <div className="sv-nav-section">collections</div>
        {builtIn.map(c => (
          <button key={c.id} className="sv-nav-item">
            <span style={{ width: 14, display: 'inline-block', textAlign: 'center', opacity: 0.7 }}>{c.glyph}</span>
            <span>{c.label}</span>
            <span className="count">{c.count}</span>
          </button>
        ))}
        {userMade.length > 0 && <div className="sv-nav-section">your own</div>}
        {userMade.map(c => (
          <button key={c.id} className="sv-nav-item">
            <span style={{ width: 14, display: 'inline-block', textAlign: 'center', opacity: 0.7 }}>{c.glyph}</span>
            <span>{c.label}</span>
            <span className="count">{c.count}</span>
          </button>
        ))}
        <div className="sv-nav-section" style={{ marginTop: 12 }}>account</div>
        {user ? (
          <>
            <div style={{ fontFamily: 'var(--vz-mono)', fontSize: 10, color: 'var(--vz-ink-40)', padding: '2px 0', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
              {user.email}
            </div>
            <button className="sv-nav-item" onClick={onSignOut}>
              <span style={{ width: 14, display: 'inline-block', textAlign: 'center', opacity: 0.7 }}>⏻</span>
              <span>sign out</span>
            </button>
          </>
        ) : (
          <div style={{ display: 'flex', gap: 6, marginTop: 4 }}>
            <button
              onClick={() => onAuthClick && onAuthClick('signin')}
              style={{
                flex: 1, padding: '7px 0', border: '1px solid var(--vz-ink-15)', borderRadius: 8,
                background: 'transparent', fontFamily: 'var(--vz-mono)', fontSize: 10,
                color: 'var(--vz-ink)', cursor: 'pointer',
              }}
            >sign in</button>
            <button
              onClick={() => onAuthClick && onAuthClick('signup')}
              style={{
                flex: 1, padding: '7px 0', border: '1px solid var(--vz-cobalt)', borderRadius: 8,
                background: 'var(--vz-cobalt)', fontFamily: 'var(--vz-mono)', fontSize: 10,
                color: 'white', cursor: 'pointer',
              }}
            >sign up</button>
          </div>
        )}
      </nav>

    </aside>
  );
}

// =================== EMPTY STATE =============================
function EmptyState({ onOpenSave }) {
  return (
    <div className="sv-empty" data-screen-label="empty">
      <div className="big-blob" />
      <h2>nothing here <em>yet.</em></h2>
      <div className="sub">that's about to change.</div>

      <div className="sv-empty-inputs">
        <div style={{
          display: 'flex',
          gap: 18,
          justifyContent: 'center',
          fontFamily: 'var(--vz-mono)',
          fontSize: 11,
          letterSpacing: '0.1em',
          textTransform: 'uppercase',
          color: 'var(--vz-ink-40)',
          marginBottom: 14,
        }}>
          <span>screenshot</span><span>·</span><span>link</span><span>·</span><span>note</span>
        </div>
        <button className="sv-save-btn" style={{ width: '100%', justifyContent: 'center' }} onClick={onOpenSave}>
          <span className="plus">+</span> save your first thing
        </button>
      </div>
    </div>
  );
}

// =================== AUTH MODAL ==============================
// Sign up / sign in modal. After signup, shows "check your email".
// Uses Supabase Auth when wired up; localStorage placeholder for now.
function AuthModal({ onClose, onAuth, mode: initialMode = 'signup' }) {
  const [mode, setMode] = useState(initialMode); // signup | signin | check-email
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  const handleGoogle = async () => {
    setLoading(true);
    setError(null);
    try {
      if (window.__supabase) {
        const { error: err } = await window.__supabase.auth.signInWithOAuth({
          provider: 'google',
          options: { redirectTo: window.location.origin },
        });
        if (err) throw err;
        // Supabase redirects to Google — auth completes on return
      } else {
        throw new Error('auth not available — please serve via http, not file://');
      }
    } catch (e) {
      setError(e.message || 'google sign-in failed');
    }
    setLoading(false);
  };

  const handleSubmit = async () => {
    if (!email || !email.includes('@')) { setError('enter a valid email'); return; }
    if (!password || password.length < 6) { setError('password must be 6+ characters'); return; }
    setLoading(true);
    setError(null);

    try {
      if (window.__supabase) {
        // Real Supabase Auth
        if (mode === 'signup') {
          const { error: err } = await window.__supabase.auth.signUp({ email, password });
          if (err) throw err;
          setMode('check-email');
        } else {
          const { data, error: err } = await window.__supabase.auth.signInWithPassword({ email, password });
          if (err) throw err;
          onAuth && onAuth({ email: data.user.email, id: data.user.id });
        }
      } else {
        throw new Error('auth not available — please serve via http, not file://');
      }
    } catch (e) {
      setError(e.message || 'something went wrong');
    }
    setLoading(false);
  };

  const inputStyle = {
    width: '100%', padding: '11px 14px', fontSize: 13,
    fontFamily: 'var(--vz-mono)', border: '1px solid var(--vz-ink-15)',
    borderRadius: 8, background: 'var(--vz-cream)', color: 'var(--vz-ink)',
    outline: 'none', boxSizing: 'border-box',
  };

  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 9999,
      background: 'rgba(15,15,18,0.6)', backdropFilter: 'blur(8px)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
    }} onClick={onClose}>
      <div style={{
        background: 'var(--vz-cream)', color: 'var(--vz-ink)', borderRadius: 20,
        padding: '36px 32px 28px', maxWidth: 380, width: '90%',
        textAlign: 'center', position: 'relative',
        border: '1px solid var(--vz-ink-15)',
      }} onClick={(e) => e.stopPropagation()}>
        <button onClick={onClose} style={{
          position: 'absolute', top: 12, right: 16,
          background: 'none', border: 'none', color: 'var(--vz-ink-40)',
          fontSize: 20, cursor: 'pointer',
        }}>×</button>

        <div style={{
          width: 48, height: 48, borderRadius: 'var(--r-blob)',
          background: 'var(--vz-orange)', margin: '0 auto 16px',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          fontSize: 24, color: 'white', transform: 'rotate(-12deg)',
        }}>✦</div>

        {mode === 'check-email' ? (
          <>
            <div style={{ fontFamily: 'var(--vz-serif)', fontSize: 22, fontWeight: 900, marginBottom: 8 }}>
              check your email
            </div>
            <div style={{ fontFamily: 'var(--vz-mono)', fontSize: 13, color: 'var(--vz-ink-60)', lineHeight: 1.6, marginBottom: 20 }}>
              we sent a confirmation link to <strong>{email}</strong>. click it, then come back here and sign in.
            </div>
            <button
              onClick={() => setMode('signin')}
              style={{
                width: '100%', padding: '12px 0', borderRadius: 10, border: 'none',
                background: 'var(--vz-ink)', color: 'var(--vz-cream)',
                fontFamily: 'var(--vz-mono)', fontSize: 13, fontWeight: 700, cursor: 'pointer',
              }}
            >i confirmed, sign me in</button>
          </>
        ) : (
          <>
            <div style={{ fontFamily: 'var(--vz-serif)', fontSize: 22, fontWeight: 900, marginBottom: 8 }}>
              {mode === 'signup' ? 'create your account' : 'welcome back'}
            </div>
            <div style={{ fontFamily: 'var(--vz-mono)', fontSize: 13, color: 'var(--vz-ink-60)', lineHeight: 1.6, marginBottom: 20 }}>
              {mode === 'signup'
                ? 'sign up to action your saves with claude.'
                : 'sign in to pick up where you left off.'}
            </div>

            <button
              onClick={handleGoogle}
              disabled={loading}
              style={{
                width: '100%', padding: '11px 0', borderRadius: 10,
                border: '1px solid var(--vz-ink-15)',
                background: 'white', color: 'var(--vz-ink)',
                fontFamily: 'var(--vz-mono)', fontSize: 13, fontWeight: 500,
                cursor: 'pointer', display: 'flex', alignItems: 'center',
                justifyContent: 'center', gap: 10,
              }}
            >
              <svg width="18" height="18" viewBox="0 0 24 24">
                <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/>
                <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
                <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
                <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
              </svg>
              continue with google
            </button>

            <div style={{
              display: 'flex', alignItems: 'center', gap: 12,
              margin: '16px 0',
            }}>
              <div style={{ flex: 1, height: 1, background: 'var(--vz-ink-15)' }} />
              <span style={{ fontFamily: 'var(--vz-mono)', fontSize: 10, color: 'var(--vz-ink-40)', textTransform: 'uppercase', letterSpacing: '0.1em' }}>or</span>
              <div style={{ flex: 1, height: 1, background: 'var(--vz-ink-15)' }} />
            </div>

            <div style={{ textAlign: 'left', display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 14 }}>
              <div>
                <div style={{ fontFamily: 'var(--vz-mono)', fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.1em', opacity: 0.6, marginBottom: 4 }}>email</div>
                <input
                  type="email" placeholder="you@example.com" value={email}
                  onChange={(e) => { setEmail(e.target.value); setError(null); }}
                  onKeyDown={(e) => { if (e.key === 'Enter') handleSubmit(); }}
                  style={inputStyle}
                />
              </div>
              <div>
                <div style={{ fontFamily: 'var(--vz-mono)', fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.1em', opacity: 0.6, marginBottom: 4 }}>password</div>
                <input
                  type="password" placeholder="6+ characters" value={password}
                  onChange={(e) => { setPassword(e.target.value); setError(null); }}
                  onKeyDown={(e) => { if (e.key === 'Enter') handleSubmit(); }}
                  style={inputStyle}
                />
              </div>
            </div>

            {error && (
              <div style={{ fontFamily: 'var(--vz-mono)', fontSize: 11, color: 'var(--vz-orange)', marginBottom: 10 }}>
                {error}
              </div>
            )}

            <button
              onClick={handleSubmit}
              disabled={loading}
              style={{
                width: '100%', padding: '12px 0', borderRadius: 10, border: 'none',
                background: loading ? 'var(--vz-ink-40)' : 'var(--vz-ink)',
                color: 'var(--vz-cream)',
                fontFamily: 'var(--vz-mono)', fontSize: 13, fontWeight: 700, cursor: 'pointer',
              }}
            >
              {loading ? '...' : mode === 'signup' ? 'create account' : 'sign in'}
            </button>

            <div style={{ marginTop: 14, fontFamily: 'var(--vz-mono)', fontSize: 11, color: 'var(--vz-ink-40)' }}>
              {mode === 'signup' ? (
                <span>already have an account? <span style={{ color: 'var(--vz-orange)', cursor: 'pointer', textDecoration: 'underline' }} onClick={() => { setMode('signin'); setError(null); }}>sign in</span></span>
              ) : (
                <span>no account? <span style={{ color: 'var(--vz-orange)', cursor: 'pointer', textDecoration: 'underline' }} onClick={() => { setMode('signup'); setError(null); }}>sign up free</span></span>
              )}
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// =================== SAVE CORE ===============================
// The inner save UI — tabs + body + footer. Used both inside the
// modal and inline on the welcome page.

// Resize image to fit within ~200KB base64 so claude.complete will accept it.
// Returns a new data URL (jpeg, quality steps down until small enough).
async function resizeImageForVision(dataUrl, maxBytes = 200000) {
  if (!dataUrl) return null;
  const img = await new Promise((resolve, reject) => {
    const i = new Image();
    i.onload = () => resolve(i);
    i.onerror = reject;
    i.src = dataUrl;
  });
  // Cap longest edge at 768px (smaller to work on mobile). Iterate quality down if still too big.
  const max = 768;
  let scale = Math.min(1, max / Math.max(img.width, img.height));
  const w = Math.round(img.width * scale);
  const h = Math.round(img.height * scale);
  const canvas = document.createElement('canvas');
  canvas.width = w;
  canvas.height = h;
  const ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0, w, h);
  for (const q of [0.8, 0.6, 0.45, 0.3, 0.2]) {
    const out = canvas.toDataURL('image/jpeg', q);
    if (out.length < maxBytes * 1.4) return out; // base64 is ~33% bigger
  }
  return canvas.toDataURL('image/jpeg', 0.2);
}

// Ask Claude to clean up a title & pick the right category.
// For screenshots, we send the actual image so it can SEE the content.
window.inferSave = inferSave;
async function inferSave(input, type, imageDataUrl) {
  const cats = Object.keys(window.SAVVIE_DATA.CATEGORIES);
  const instruction = `You are Savvie, a saved-items app. Read the image and the user's note carefully.

Pick a SHORT clean title — **MAX 5 WORDS**, lowercase, no quotes, no period, no file extensions. Describe what the saved thing IS based on what you see — pull names of brands, accounts, products, dishes, places. Never say "screenshot of...".

Pick the best-fit category from this list: ${cats.join(', ')}.

Identify the platform (instagram, tiktok, youtube, x, reddit, linkedin, pinterest, web, note) and the account/source name if visible (e.g. @wallstreethq, @droppablestudio, "serious eats"). If you can't tell, use "web" for platform and "" for account.

User's note: ${input || '(no extra context)'}

Examples (note: each title is 5 words or fewer):
- a tiktok showing pinterest + claude code + stitch → {"title":"pinterest + claude code workflow","category":"workflow","platform":"tiktok","account":""}
- a recipe post for miso glazed eggplant → {"title":"miso glazed eggplant","category":"recipe","platform":"instagram","account":"@seriouseats"}
- an instagram post from @wallstreethq about nvidia → {"title":"nvidia top 5 holdings jensen","category":"read-later","platform":"instagram","account":"@wallstreethq"}
- a tutorial on capcut compositing → {"title":"capcut composite workflow","category":"tutorial","platform":"youtube","account":""}

Respond with ONLY a JSON object on one line: {"title":"...","category":"...","platform":"...","account":"..."}`;

  let smallImage = null;
  if (imageDataUrl && type === 'screenshot') {
    try {
      console.log('[savvie] resizing image for vision…', 'dataUrl length:', imageDataUrl.length);
      smallImage = await resizeImageForVision(imageDataUrl);
      console.log('[savvie] resize done, smallImage length:', smallImage?.length || 0);
    } catch (e) {
      console.warn('[savvie] resize FAILED:', e.message || e);
      // resize failed; fall through to text-only
    }
  }

  // Try vision (with resized image) first, then plain text as backup.
  const attempts = [];
  if (smallImage) {
    const m = smallImage.match(/^data:([^;]+);base64,(.+)$/);
    if (m) {
      console.log('[savvie] sending vision request, base64 size:', m[2].length);
      // Pre-log for debug bar
      window.__dbgVision = `img: ${(m[2].length/1024).toFixed(0)}KB, key: ${window.__savvieApiKey ? window.__savvieApiKey.slice(0,12) + '…' : 'NONE'}`;
      attempts.push({
        messages: [{
          role: 'user',
          content: [
            { type: 'image', source: { type: 'base64', media_type: m[1], data: m[2] } },
            { type: 'text', text: instruction },
          ],
        }],
      });
    }
  }
  attempts.push(instruction);

  // Temporary on-screen debug (remove after fixing) — appends each line
  const _dbg = (msg) => {
    console.log('[savvie]', msg);
    if (!window.__savvieDebug) {
      const d = document.createElement('div');
      Object.assign(d.style, {
        position: 'fixed', bottom: '60px', left: '8px', right: '8px',
        background: '#111', color: '#0f0', fontFamily: 'monospace',
        fontSize: '10px', padding: '8px 10px', borderRadius: '8px',
        zIndex: 99999, maxHeight: '200px', overflow: 'auto', wordBreak: 'break-all',
        lineHeight: '1.6',
      });
      d.textContent = '';
      document.body.appendChild(d);
      window.__savvieDebug = d;
    }
    window.__savvieDebug.textContent += msg + '\n';
    window.__savvieDebug.scrollTop = 99999;
  };

  if (window.__dbgVision) { _dbg(window.__dbgVision); window.__dbgVision = null; }
  _dbg(`${attempts.length} attempt(s) queued`);

  for (let ai = 0; ai < attempts.length; ai++) {
    const payload = attempts[ai];
    try {
      const label = ai === 0 && smallImage ? 'vision' : 'text-only';
      _dbg(`attempt ${ai + 1}/${attempts.length} (${label})…`);
      const result = await window.claude.complete(payload);
      const text = typeof result === 'string' ? result : (result?.content || result?.text || '');
      _dbg(`response: ${text.slice(0, 120)}`);
      const match = text.match(/\{[\s\S]*?\}/);
      if (!match) { console.warn('[savvie] no JSON found in response'); continue; }
      const parsed = JSON.parse(match[0]);
      if (parsed.title && parsed.category && cats.includes(parsed.category)) {
        let cleaned = String(parsed.title)
          .toLowerCase()
          .replace(/^["']|["']$/g, '')
          .replace(/\.[a-z]+$/i, '')
          .replace(/\.$/, '')
          .trim();
        const words = cleaned.split(/\s+/);
        if (words.length > 5) cleaned = words.slice(0, 5).join(' ');
        console.log('[savvie] inferred:', cleaned, '→', parsed.category);
        return {
          title: cleaned,
          category: parsed.category,
          platform: parsed.platform || '',
          account: parsed.account || '',
        };
      }
      console.warn('[savvie] parsed but bad data:', parsed);
    } catch (e) {
      _dbg(`attempt ${ai + 1} FAILED: ${e.message || e}`);
    }
  }
  _dbg('all attempts failed — using fallback title');
  return null;
}

// Fallback if Claude can't (or won't) help. Generates something readable.
function fallbackTitle(hint, type) {
  if (!hint) return type === 'screenshot' ? 'new screenshot save' : 'new save';
  // Strip filename-y patterns like "IMG_1991.PNG" or "pasted.png"
  const looksLikeFilename = /^(img[_-]?\d+|pasted|screen.?shot|untitled|file|photo|image|dcim)/i.test(hint) || /\.(png|jpe?g|gif|webp)$/i.test(hint);
  if (looksLikeFilename) {
    const now = new Date();
    const hh = String(now.getHours()).padStart(2, '0');
    const mm = String(now.getMinutes()).padStart(2, '0');
    const labels = ['saved screenshot', 'new save', 'dropped a screenshot', 'screenshot save'];
    const pick = labels[Math.floor(Math.random() * labels.length)];
    return `${pick} · ${hh}:${mm}`;
  }
  return hint.length > 80 ? hint.slice(0, 80) + '…' : hint;
}

function SaveCore({ onSave, autoFocus = true, compact = false, user, onAuthClick }) {
  const [tab, setTab] = useState('screenshot');
  const [linkVal, setLinkVal] = useState('');
  const [noteVal, setNoteVal] = useState('');
  const [pretext, setPretext] = useState(null);
  const [drag, setDrag] = useState(false);
  // Multi-file: array of { name, preview, _id }
  const [files, setFiles] = useState([]);
  const [caption, setCaption] = useState('');
  const [categorizing, setCategorizing] = useState(false);
  const [categorizingText, setCategorizingText] = useState('figuring it out…');
  const [showTrialGate, setShowTrialGate] = useState(false);
  const fileInputRef = React.useRef(null);

  // Saves are unlimited — auth only gates actioning
  const isTrialUser = false;

  const MAX_FILES = 10;
  const { REMINDER_PRETEXTS } = window.SAVVIE_DATA;

  const addFiles = (newFiles) => {
    const arr = Array.from(newFiles || []);
    if (arr.length === 0) return;
    const room = Math.max(0, MAX_FILES - files.length);
    arr.slice(0, room).forEach((f) => {
      const reader = new FileReader();
      reader.onload = (e) => {
        setFiles((prev) => prev.length >= MAX_FILES
          ? prev
          : [...prev, { name: f.name, preview: e.target.result, _id: Math.random() }]);
      };
      reader.readAsDataURL(f);
    });
  };

  const removeFile = (id) => setFiles((prev) => prev.filter((f) => f._id !== id));

  const handlePaste = async () => {
    try {
      if (navigator.clipboard && navigator.clipboard.read) {
        const items = await navigator.clipboard.read();
        for (const item of items) {
          for (const type of item.types) {
            if (type.startsWith('image/')) {
              const blob = await item.getType(type);
              addFiles([new File([blob], 'pasted.png', { type })]);
              return;
            }
          }
        }
      }
      setFiles((prev) => [...prev, { name: 'pasted screenshot', preview: null, _id: Math.random() }]);
    } catch (err) {
      setFiles((prev) => [...prev, { name: 'pasted screenshot', preview: null, _id: Math.random() }]);
    }
  };

  const handleSave = async () => {
    setCategorizing(true);

    // Build the list of inputs (1 for link/note · up to 10 for screenshots)
    let inputs = [];
    if (tab === 'screenshot') {
      if (files.length === 0) {
        inputs.push({ type: 'screenshot', hint: caption || 'unlabelled screenshot', thumb: null });
      } else {
        inputs = files.map((f) => ({
          type: 'screenshot',
          hint: caption ? `${caption} · ${f.name}` : f.name,
          thumb: f.preview,
        }));
      }
    } else if (tab === 'link') {
      const links = linkVal.split('\n').map(l => l.trim()).filter(Boolean);
      if (links.length === 0) {
        inputs.push({ type: 'link', hint: 'untitled link', thumb: null });
      } else {
        links.forEach(link => {
          inputs.push({ type: 'link', hint: link, thumb: null });
        });
      }
    } else {
      // Reminders: always 1 save — keep the whole note together
      const fullNote = noteVal.trim();
      if (!fullNote) {
        inputs.push({ type: 'note', hint: pretext ? `${pretext}: untitled reminder` : 'untitled reminder', thumb: null });
      } else {
        const fullInput = pretext ? `${pretext}: ${fullNote}` : fullNote;
        inputs.push({ type: 'note', hint: fullInput, thumb: null });
      }
    }

    // Playful rotating loading messages
    const funSingle = [
      'reading your mind…',
      'ooh what is this…',
      'claude is squinting…',
      'sorting your chaos…',
      'one sec, thinking…',
      'hmm, interesting…',
    ];
    const funMulti = [
      `${inputs.length} saves?! ambitious.`,
      `speed-reading ${inputs.length} things…`,
      `juggling ${inputs.length} saves…`,
      `${inputs.length} at once — respect.`,
      `sorting ${inputs.length} saves, no sweat…`,
      `claude is multitasking ${inputs.length}…`,
    ];
    const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
    setCategorizingText(inputs.length > 1 ? pick(funMulti) : pick(funSingle));

    // Categorize in parallel via claude.complete (with vision for screenshots)
    const results = await Promise.all(
      inputs.map(async (item) => {
        let inferred = null;
        try {
          inferred = await inferSave(item.hint, item.type, item.thumb);
        } catch (err) {
          console.error('[savvie] inferSave threw:', err);
        }
        const fallbackCat = item.type === 'link' ? 'read-later'
          : item.type === 'note' ? 'home'
          : 'home';
        // If vision failed for a screenshot, show a temporary debug toast (remove later)
        if (!inferred && item.type === 'screenshot' && item.thumb) {
          console.warn('[savvie] vision failed for screenshot, using fallback title');
        }
        return {
          type: item.type,
          title: inferred ? inferred.title : fallbackTitle(item.hint, item.type),
          category: inferred ? inferred.category : fallbackCat,
          platform: inferred?.platform || item.type,
          account: inferred?.account || '',
          thumb: item.thumb,
        };
      })
    );
    results.forEach((r) => onSave(r));

    // Increment trial counter
    try { localStorage.setItem('savvie-saves-used', String(savesUsed + results.length)); } catch(e) {}

    setCategorizing(false);
    setLinkVal(''); setNoteVal(''); setPretext(null);
    setFiles([]); setCaption('');
    setTab('screenshot');
  };

  return (
    <>
      <div className="sv-modal-tabs">
        {[
          { id: 'screenshot', label: 'screenshot', glyph: '▢' },
          { id: 'link',       label: 'link',       glyph: '↗' },
          { id: 'reminder',   label: 'reminder',   glyph: '✦' },
        ].map(t => (
          <button
            key={t.id}
            className={`sv-modal-tab ${tab === t.id ? 'active' : ''}`}
            onClick={() => setTab(t.id)}
          >
            <span className="glyph">{t.glyph}</span>{t.label}
          </button>
        ))}
      </div>

      <div className="sv-modal-body">
        {tab === 'screenshot' && (
          <div
            className={`sv-dropzone ${drag ? 'dragover' : ''} ${files.length > 0 ? 'has-files' : ''}`}
            onDragOver={(e) => { e.preventDefault(); setDrag(true); }}
            onDragLeave={() => setDrag(false)}
            onDrop={(e) => {
              e.preventDefault();
              setDrag(false);
              addFiles(e.dataTransfer.files);
            }}
          >
            <input
              ref={fileInputRef}
              type="file"
              accept="image/*"
              multiple
              style={{ display: 'none' }}
              onChange={(e) => { addFiles(e.target.files); e.target.value = ''; }}
            />
            <div className="corner-blob" />
            <div className="corner-blob bl" />

            {files.length === 0 ? (
              <>
                <div className="hero-blob" />
                <div className="copy">drop a screenshot.<br/>we'll figure it out.</div>
                <div className="sub">drag. pick. paste. one image per save. up to 10.</div>
                <div className="file-options">
                  <button onClick={() => fileInputRef.current?.click()}>pick files</button>
                  <button onClick={handlePaste}>paste from clipboard</button>
                </div>
              </>
            ) : (
              <>
                <div className="sv-thumb-grid">
                  {files.map((f) => (
                    <div key={f._id} className="sv-thumb">
                      {f.preview ? (
                        <img src={f.preview} alt={f.name} />
                      ) : (
                        <div className="sv-thumb-placeholder">
                          <span className="sv-thumb-blob" />
                        </div>
                      )}
                      <button className="sv-thumb-x" onClick={() => removeFile(f._id)} title="remove">×</button>
                    </div>
                  ))}
                  {files.length < MAX_FILES && (
                    <button
                      className="sv-thumb-add"
                      onClick={() => fileInputRef.current?.click()}
                      title={`add up to ${MAX_FILES - files.length} more`}
                    >
                      <span>+</span>
                      <span className="lbl">add</span>
                    </button>
                  )}
                </div>
                <input
                  className="sv-caption-input"
                  placeholder="optional · what are these about? (helps claude file them)"
                  value={caption}
                  onChange={(e) => setCaption(e.target.value)}
                />
                <div className="sub">
                  {files.length} of {MAX_FILES} · claude categorizes each on save
                </div>
              </>
            )}
          </div>
        )}

        {tab === 'link' && (
          <div className="sv-link-tab">
            <textarea
              className="sv-link-input"
              placeholder="paste a link · enter to save\nshift+enter for multiple links"
              value={linkVal}
              onChange={(e) => setLinkVal(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === 'Enter' && !e.shiftKey && linkVal.trim()) {
                  e.preventDefault();
                  handleSave();
                }
              }}
              autoFocus={autoFocus}
              style={{ minHeight: 60, resize: 'vertical', fontFamily: 'var(--vz-mono)', fontSize: 14 }}
            />
            <div className="sv-link-preview">
              {linkVal.trim() ? (
                <>
                  <span className="lbl">{linkVal.trim().split('\n').filter(Boolean).length} link{linkVal.trim().split('\n').filter(Boolean).length !== 1 ? 's' : ''} ready to save</span>
                  {linkVal.trim().split('\n').filter(Boolean).map((url, i) => (
                    <span key={i} style={{ fontFamily: 'var(--vz-mono)', color: 'var(--vz-ink-60)', fontSize: 12, display: 'block', marginTop: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{url.trim()}</span>
                  ))}
                </>
              ) : (
                <>
                  <span className="lbl">paste one or more links</span>
                  <span className="hand">enter to save · shift+enter for more lines.</span>
                </>
              )}
            </div>
          </div>
        )}

        {tab === 'reminder' && (
          <div className="sv-reminder-tab">
            <div className="sv-pretext-row">
              <span className="lbl">pretext ·</span>
              {REMINDER_PRETEXTS.map(p => (
                <button
                  key={p.prefix}
                  className={`sv-pretext ${pretext === p.prefix ? 'active' : ''}`}
                  onClick={() => setPretext(pretext === p.prefix ? null : p.prefix)}
                >
                  <span className="swatch" style={{ background: `var(--vz-${p.color})` }} />
                  {p.prefix}
                </button>
              ))}
              {!pretext && (
                <span className="sv-pretext-hint">↙ pick one</span>
              )}
            </div>

            <div className="sv-reminder-stage">
              {pretext && <span className="ribbon">{pretext}:</span>}
              <textarea
                placeholder={
                  pretext === 'checkout' ? 'a product, a shop, a tool…' :
                  pretext === 'plan' ? 'a trip, a dinner, a project…' :
                  pretext === 'watch' ? 'a film, a talk, a reel…' :
                  pretext === 'read' ? 'an article, a paper, a thread…' :
                  pretext === 'try' ? 'a tool, a recipe, a place…' :
                  pretext === 'ask' ? 'a person or claude · what to ask…' :
                  pretext === 'buy' ? 'a thing you want…' :
                  pretext === 'cook' ? 'a meal worth making…' :
                  'e.g. check out mirofish\npaste multiple lines for bulk saves'
                }
                value={noteVal}
                onChange={(e) => setNoteVal(e.target.value)}
                onKeyDown={(e) => {
                  if (e.key === 'Enter' && !e.shiftKey) {
                    e.preventDefault();
                    if (noteVal.trim()) handleSave();
                  }
                }}
                autoFocus={autoFocus}
              />
            </div>

            <div className="sv-reminder-hint">
              enter to save · paste multiple lines for bulk saves · shift+enter for new line.
            </div>
          </div>
        )}
      </div>

      <div className="sv-modal-foot">
        <div className="sv-modal-hint">
          {isTrialUser && savesLeft > 0 ? (
            <span style={{ fontFamily: 'var(--vz-mono)', fontSize: 10, color: savesLeft <= 3 ? 'var(--vz-orange)' : 'var(--vz-ink-40)' }}>
              {savesLeft} free save{savesLeft !== 1 ? 's' : ''} left
            </span>
          ) : isTrialUser ? (
            <span style={{ fontFamily: 'var(--vz-mono)', fontSize: 10, color: 'var(--vz-orange)' }}>
              trial ended · <span style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={() => onAuthClick && onAuthClick('signup')}>sign up</span>
            </span>
          ) : (
            <span style={{ fontFamily: 'var(--vz-mono)', fontSize: 10, color: 'var(--vz-cobalt)' }}>
              ✓ signed in
            </span>
          )}
        </div>
        <button className="sv-save-btn" onClick={handleSave} disabled={categorizing}>
          {categorizing
            ? <><span style={{
                display: 'inline-block',
                width: 16, height: 16,
                background: 'var(--vz-orange)',
                border: '1.5px solid var(--vz-ink)',
                borderRadius: 'var(--r-blob)',
                animation: 'blob-morph 1.8s linear infinite, save-bounce 0.8s ease-in-out infinite',
                transformOrigin: 'center',
              }} /> {categorizingText}</>
            : <><span className="plus">+</span> save{files.length > 1 ? ` all ${files.length}` : ''}</>}
        </button>
      </div>

      {showTrialGate && (
        <AuthModal mode="signup" onClose={() => setShowTrialGate(false)} onAuth={(u) => { setShowTrialGate(false); onAuthClick && onAuthClick('authed', u); }} />
      )}
    </>
  );
}

// =================== SAVE MODAL ==============================
function SaveModal({ onClose, onSave, user, onAuthClick }) {
  useEffect(() => {
    const onEsc = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onEsc);
    return () => window.removeEventListener('keydown', onEsc);
  }, [onClose]);

  return (
    <div className="sv-modal-veil" onClick={onClose}>
      <div className="sv-modal" onClick={(e) => e.stopPropagation()} data-screen-label="save-modal">
        <button className="close" onClick={onClose}>×</button>
        <div className="sv-modal-head">
          <h2>drop your <em>save.</em></h2>
          <div className="modal-sub">we'll figure out where it goes.</div>
        </div>
        <SaveCore onSave={onSave} user={user} onAuthClick={onAuthClick} />
      </div>
    </div>
  );
}

window.SaveCore = SaveCore;

// =================== DETAIL PANEL ============================
// Pick an action → see the prompt → copy → paste in Cowork
function ItemDetail({ item, onClose, onActioned, user, onAuthClick }) {
  const [activePrompt, setActivePrompt] = useState(null); // { action, prompt, copied }

  useEffect(() => {
    const onEsc = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onEsc);
    return () => window.removeEventListener('keydown', onEsc);
  }, [onClose]);

  const pickAction = (action) => {
    const base = action.prompt
      ? action.prompt.replaceAll('{title}', item.title)
      : `${action.label} for "${item.title}".`;
    // Build smart context line
    const srcParts = [];
    if (item.platform && item.platform !== item.source && item.platform !== 'screenshot') {
      srcParts.push(item.platform);
    }
    if (item.source && item.source !== 'screenshot' && item.source !== 'link') {
      srcParts.push(item.source);
    }
    const srcLine = srcParts.length > 0 ? `\nsource: ${srcParts.join(' · ')}` : '';
    const prompt = `${base}\n\ncontext: ${item.note}${srcLine}`;
    setActivePrompt({ action, prompt, copied: false });
  };

  const copyPrompt = async () => {
    if (!activePrompt) return;
    if (!user) {
      onAuthClick && onAuthClick('signup');
      return;
    }
    try {
      await navigator.clipboard.writeText(activePrompt.prompt);
      setActivePrompt({ ...activePrompt, copied: true });
      setTimeout(() => setActivePrompt(prev => prev ? { ...prev, copied: false } : null), 3000);
    } catch (e) {}
  };

  if (!item) return null;

  return (
    <div className="sv-detail-veil" onClick={onClose}>
      <aside className="sv-detail" onClick={(e) => e.stopPropagation()} data-screen-label="item-detail">
        <button className="close" onClick={onClose}>×</button>

        <div className="sv-detail-toolbar">
          <span className="sv-cat-pill">
            <span className="glyph">{item.cat.emoji}</span>
            {item.cat.label}
          </span>
          <span style={{ fontFamily: 'var(--vz-mono)', fontSize: 11, color: 'var(--vz-ink-40)' }}>
            saved {item.age} ago
          </span>
        </div>

        <h1>{item.title}</h1>

        {item.thumb && (
          <div className="sv-detail-thumb">
            <img src={item.thumb} alt={item.title} />
          </div>
        )}

        {item.note && (
          <div className="sv-detail-section">
            <h4>why you saved it</h4>
            <p className="sv-detail-desc">{item.note}</p>
          </div>
        )}

        <div className="sv-detail-section">
          <h4>pick an action</h4>
          <div className="sv-actions-grid">
            {[
              { id: 'pull-this', label: 'pull this for me', desc: 'fetch it · extract it · read it.', prompt: 'pull this for me: "{title}". find the original source, extract the full content, and give me: what it is, the key info that matters, and anything i should act on. be specific and useful.' },
              ...item.actions
            ].map((action, i) => (
              <button
                key={action.id}
                className={`sv-action-card ${i === 0 ? 'primary' : ''} ${activePrompt?.action.id === action.id ? 'selected' : ''}`}
                onClick={() => pickAction(action)}
              >
                <div className="label">{action.label}</div>
                <div className="desc">{action.desc}</div>
              </button>
            ))}
          </div>
        </div>

        {activePrompt && (
          <div style={{
            marginTop: 16,
            background: 'var(--vz-ink)',
            color: 'var(--vz-cream)',
            borderRadius: 12,
            padding: '16px 18px',
            fontFamily: 'var(--vz-mono)',
            fontSize: 12,
            lineHeight: 1.6,
            position: 'relative',
          }}>
            <div style={{
              display: 'flex',
              justifyContent: 'space-between',
              alignItems: 'center',
              marginBottom: 10,
              fontFamily: 'var(--vz-sans)',
              fontSize: 11,
              textTransform: 'uppercase',
              letterSpacing: '0.08em',
              opacity: 0.5,
            }}>
              <span>your prompt · paste this in cowork</span>
            </div>
            <div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
              {activePrompt.prompt}
            </div>
            <div style={{ display: 'flex', gap: 8, marginTop: 14 }}>
              <button
                onClick={copyPrompt}
                style={{
                  flex: 1,
                  padding: '10px 0',
                  border: '1px solid rgba(255,255,255,0.2)',
                  borderRadius: 8,
                  background: activePrompt.copied ? 'rgba(255,255,255,0.15)' : 'transparent',
                  color: activePrompt.copied ? 'var(--vz-lime)' : 'rgba(255,255,255,0.6)',
                  fontFamily: 'var(--vz-mono)',
                  fontSize: 12,
                  cursor: 'pointer',
                  letterSpacing: '0.04em',
                  transition: 'all 0.2s ease',
                }}
              >
                {activePrompt.copied ? '✓ copied' : 'copy prompt'}
              </button>
              <button
                onClick={() => {
                  if (!activePrompt) return;
                  if (!user) {
                    onAuthClick && onAuthClick('signup');
                    return;
                  }
                  // Copy prompt to clipboard
                  try { navigator.clipboard.writeText(activePrompt.prompt); } catch(e) {}
                  // Deep link to Cowork
                  const encoded = encodeURIComponent(activePrompt.prompt);
                  const coworkUrl = 'claude://cowork/new?q=' + encoded;

                  const a = document.createElement('a');
                  a.href = coworkUrl;
                  a.style.display = 'none';
                  document.body.appendChild(a);
                  a.click();
                  setTimeout(() => document.body.removeChild(a), 100);

                  // Show "switch to Claude" nudge
                  setActivePrompt({ ...activePrompt, sent: true });
                }}
                style={{
                  flex: 2,
                  padding: '12px 0',
                  border: '1px solid var(--vz-lime)',
                  borderRadius: 8,
                  background: 'var(--vz-lime)',
                  color: 'var(--vz-ink)',
                  fontFamily: 'var(--vz-mono)',
                  fontSize: 13,
                  fontWeight: 700,
                  cursor: 'pointer',
                  letterSpacing: '0.04em',
                }}
              >
                {activePrompt.sent ? '✓ sent to cowork' : 'run in cowork →'}
              </button>
            </div>
            {activePrompt.sent && (
              <div style={{
                marginTop: 10,
                padding: '10px 14px',
                background: 'var(--vz-lime)',
                borderRadius: 10,
                fontFamily: 'var(--vz-mono)',
                fontSize: 11,
                color: 'var(--vz-ink)',
                display: 'flex',
                alignItems: 'center',
                gap: 8,
                animation: 'fadeIn 0.3s ease',
              }}>
                <span style={{ fontSize: 16 }}>⌘</span>
                <span>prompt sent — <strong>switch to claude</strong> to run it. prompt also copied to clipboard.</span>
              </div>
            )}
          </div>
        )}


        <div style={{
          marginTop: 20,
          paddingTop: 14,
          borderTop: '1px dashed var(--vz-ink-15)',
          display: 'flex',
          gap: 10,
          justifyContent: 'flex-end',
        }}>
          <button className="sv-btn" onClick={onClose}>close</button>
          <button
            className="sv-btn"
            style={{
              background: 'var(--vz-lime)',
              color: 'var(--vz-ink)',
              border: '1px solid var(--vz-ink)',
              fontWeight: 600,
            }}
            onClick={() => onActioned(item)}
          >done · actioned ✓</button>
        </div>
      </aside>
    </div>
  );
}

// =================== TOAST ===================================
function Toast({ message, hand, onDone }) {
  useEffect(() => {
    const t = setTimeout(onDone, 2400);
    return () => clearTimeout(t);
  }, [onDone]);
  return (
    <div className="sv-toast">
      <span className="label">saved</span>
      <span className="vz-hand">{hand}</span>
      <span>{message}</span>
    </div>
  );
}

Object.assign(window, {
  Sidebar, EmptyState, SaveModal, ItemDetail, Toast,
});

// =================== CELEBRATION ============================
// Full-screen blob + check + caption + confetti when an action lands.
function Celebration({ caption, onDone }) {
  useEffect(() => {
    const t = setTimeout(onDone, 1800);
    return () => clearTimeout(t);
  }, [onDone]);

  // 14 confetti pebbles scattered around the blob
  const confetti = Array(14).fill(0).map((_, i) => {
    const angle = (i / 14) * Math.PI * 2;
    const dist = 140 + Math.random() * 100;
    const x = Math.cos(angle) * dist;
    const y = Math.sin(angle) * dist - 40;
    const colors = ['var(--vz-orange)', 'var(--vz-cobalt)', 'var(--vz-lime)', 'var(--vz-pink)'];
    return {
      x, y,
      bg: colors[i % colors.length],
      size: 8 + Math.random() * 10,
      delay: Math.random() * 80,
    };
  });

  return (
    <div className="sv-celebration">
      <div className="stage">
        <div className="blob" />
        <div className="check">✓</div>
        {confetti.map((c, i) => (
          <span
            key={i}
            className="confetti"
            style={{
              left: '50%', top: '50%',
              background: c.bg,
              width: c.size, height: c.size,
              animationDelay: `${c.delay}ms`,
              '--end': `translate(${c.x}px, ${c.y}px)`,
            }}
          />
        ))}
      </div>
      <div className="celeb-caption">{caption || 'done.'}</div>
    </div>
  );
}

// =================== RE-ORG OVERLAY ==========================
// Brief overlay while items are being re-categorized.
function ReorgOverlay({ caption = 'claude is re-organizing…' }) {
  return (
    <div className="sv-reorg-overlay">
      <div className="blob-pile">
        <span /><span /><span /><span />
      </div>
      <div className="caption">re-shuffling.</div>
      <div className="sub">{caption}</div>
    </div>
  );
}

window.Celebration = Celebration;
window.ReorgOverlay = ReorgOverlay;
