// Main app shell — wired to the live-gallery FastAPI backend

function App() {
  // Auth — null=loading, true/false
  const [authed, setAuthed] = React.useState(null);
  const [displayName, setDisplayName] = React.useState('');
  const [theme, setTheme] = React.useState(() => localStorage.getItem('studio.theme') || 'graphite');
  const [soundOn, setSoundOn] = React.useState(() => localStorage.getItem('studio.sound') !== 'off');

  // Onboarding & profile
  const [showWelcome, setShowWelcome] = React.useState(false);
  const [showProfilePhoto, setShowProfilePhoto] = React.useState(false);
  const [showBranding, setShowBranding] = React.useState(false);
  const [profilePhotoUrl, setProfilePhotoUrl] = React.useState(null);

  // Sidebar
  const [sidebarOpen, setSidebarOpen] = React.useState(() => window.innerWidth >= 768);

  // View state
  const [view, setView] = React.useState('grid');
  const [sort, setSort] = React.useState('capture_desc');
  const [density, setDensity] = React.useState('normal');
  const [searchQuery, setSearchQuery] = React.useState('');
  const [analyticsOpen, setAnalyticsOpen] = React.useState(false);

  // Filters
  const [tagFilter, setTagFilter] = React.useState(null);
  const [ratingFilter, setRatingFilter] = React.useState(0);
  const [flagFilter, setFlagFilter] = React.useState('all');
  const [fileTypeFilter, setFileTypeFilter] = React.useState('all');
  const [activeCollection, setActiveCollection] = React.useState(null);
  const [toolbarFilter, setToolbarFilter] = React.useState('all');
  const [colorFilter, setColorFilter] = React.useState(null);
  const [collectionsDrawerOpen, setCollectionsDrawerOpen] = React.useState(false);

  // Selection + focus
  const [selected, setSelected] = React.useState(new Set());
  const [focused, setFocused] = React.useState(0);
  const [lightboxIdx, setLightboxIdx] = React.useState(null);

  // Lightbox state
  const [showExif, setShowExif] = React.useState(false);
  const [zoom, setZoom] = React.useState(false);
  const [baMode, setBaMode] = React.useState(false);
  const [devOpen, setDevOpen] = React.useState(false);

  // Modals
  const [shareOpen, setShareOpen] = React.useState(false);
  const [watermarkOpen, setWatermarkOpen] = React.useState(false);
  const [newCollectionOpen, setNewCollectionOpen] = React.useState(false);
  const [shortcutsOpen, setShortcutsOpen] = React.useState(false);
  const [shareViewOpen, setShareViewOpen] = React.useState(false);

  // Server data
  const [collections, setCollections] = React.useState([]);
  const [shares, setShares] = React.useState([]);
  const [watermarkCfg, setWatermarkCfg] = React.useState({ text: '', opacity: 65, position: 'bottom-right', size: 'medium' });
  const [files, setFiles] = React.useState([]);
  const [loading, setLoading] = React.useState(false);
  const [storageInfo, setStorageInfo] = React.useState(null);

  // Edits (local, save on demand)
  const [edits, setEdits] = React.useState({});

  // EXIF cache (lazy)
  const [exifCache, setExifCache] = React.useState({});

  // Tether / capture
  const [capturing, setCapturing] = React.useState(false);
  const [lastCapture, setLastCapture] = React.useState('');

  // Share preview feedback
  const [feedback, setFeedback] = React.useState(new Set());

  // Events
  const [events, setEvents] = React.useState([]);
  const [activeEvent, setActiveEvent] = React.useState(null);   // id or null (all)
  const [activeShootId, setActiveShootId] = React.useState(null); // event receiving new photos
  const [eventModalOpen, setEventModalOpen] = React.useState(false);

  // Client moodboards (from share links)
  const [moodboards, setMoodboards] = React.useState([]);

  // Second screen BroadcastChannel
  const bcRef = React.useRef(null);
  React.useEffect(() => {
    try { bcRef.current = new BroadcastChannel('livegallery-secondscreen'); } catch {}
    return () => { try { bcRef.current && bcRef.current.close(); } catch {} };
  }, []);

  const broadcastPhoto = React.useCallback((file) => {
    if (!file || !bcRef.current) return;
    try {
      bcRef.current.postMessage({
        type: 'photo',
        filename: file.filename,
        url: file.thumb_url,
        pick: !!file.pick,
        reject: !!file.reject,
        rating: file.rating || 0,
        color: file.color || null,
        tags: file.tags || [],
      });
    } catch {}
  }, []);

  const openSecondScreen = React.useCallback(() => {
    window.open('/secondscreen', '_blank', 'noopener');
  }, []);

  // Toast notifications
  const [toasts, setToasts] = React.useState([]);
  const addToast = React.useCallback((msg, type = 'error') => {
    const id = Date.now() + Math.random();
    setToasts(ts => [...ts, { id, msg, type }]);
    setTimeout(() => setToasts(ts => ts.filter(t => t.id !== id)), 4500);
  }, []);

  // ── Auth check on mount ──────────────────────────────────────────────────
  React.useEffect(() => {
    fetch('/auth/me')
      .then(r => r.ok ? r.json() : null)
      .then(d => {
        if (d) {
          setAuthed(true);
          setDisplayName(d.display_name || d.username || '');
          if (d.first_login) setShowWelcome(true);
          setProfilePhotoUrl(`/api/profile/photo?t=${Date.now()}`);
        } else {
          setAuthed(false);
        }
      })
      .catch(() => setAuthed(false));
  }, []);

  // ── Load data when authed ─────────────────────────────────────────────────
  React.useEffect(() => {
    if (!authed) return;

    setLoading(true);
    fetch('/api/gallery')
      .then(r => { if (!r.ok) { if (r.status === 401) setAuthed(false); throw new Error(); } return r.json(); })
      .then(data => { setFiles(data); setLoading(false); })
      .catch(() => setLoading(false));

    fetch('/api/collections').then(r => r.ok ? r.json() : []).then(setCollections).catch(() => {});
    fetch('/api/shares').then(r => r.ok ? r.json() : []).then(setShares).catch(() => {});
    fetch('/api/moodboards').then(r => r.ok ? r.json() : []).then(setMoodboards).catch(() => {});
    fetch('/api/events').then(r => r.ok ? r.json() : { events: [], active_id: null })
      .then(d => { setEvents(d.events || []); setActiveShootId(d.active_id || null); })
      .catch(() => {});
    fetch('/api/watermark').then(r => r.ok ? r.json() : null).then(d => d && setWatermarkCfg(p => ({ ...p, ...d }))).catch(() => {});
    fetch('/api/storage').then(r => r.ok ? r.json() : null).then(d => d && setStorageInfo(d)).catch(() => {});
  }, [authed]);

  // ── WebSocket — real-time tether ──────────────────────────────────────────
  React.useEffect(() => {
    if (!authed) return;
    const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
    let ws;
    try {
      ws = new WebSocket(`${proto}//${location.host}/ws`);
      ws.onmessage = (e) => {
        try {
          const msg = JSON.parse(e.data);
          if (msg.event === 'new_file') {
            setCapturing(false);
            setLastCapture('just now');
            setFiles(fs => [{ ...msg, isNew: true }, ...fs]);
            setTimeout(() => setFiles(fs => fs.map(f => f.filename === msg.filename ? { ...f, isNew: false } : f)), 1200);
            // Update event photo count
            if (msg.event_id) {
              setEvents(es => es.map(e => e.id === msg.event_id ? { ...e, count: (e.count || 0) + 1 } : e));
            }
            // Push new capture to second screen immediately
            try {
              if (bcRef.current) {
                bcRef.current.postMessage({
                  type: 'photo',
                  filename: msg.filename,
                  url: msg.thumb_url,
                  pick: false, reject: false, rating: 0,
                  color: null, tags: [],
                  isNew: true,
                });
              }
            } catch {}
          }
        } catch {}
      };
    } catch {}
    return () => { if (ws) ws.close(); };
  }, [authed]);

  // ── Theme ─────────────────────────────────────────────────────────────────
  React.useEffect(() => {
    document.body.dataset.theme = theme;
    localStorage.setItem('studio.theme', theme);
  }, [theme]);

  // ── Shutter sound ──────────────────────────────────────────────────────────
  const playShutter = React.useCallback(() => {
    if (!soundOn) return;
    try {
      const ctx = new (window.AudioContext || window.webkitAudioContext)();
      // Two-layer click: a short mechanical tick + a body thud
      const tick = ctx.createBuffer(1, Math.floor(ctx.sampleRate * 0.018), ctx.sampleRate);
      const thud = ctx.createBuffer(1, Math.floor(ctx.sampleRate * 0.055), ctx.sampleRate);
      const td = tick.getChannelData(0);
      const bd = thud.getChannelData(0);
      for (let i = 0; i < td.length; i++) td[i] = (Math.random()*2-1) * Math.exp(-i/(ctx.sampleRate*0.003));
      for (let i = 0; i < bd.length; i++) bd[i] = (Math.random()*2-1) * Math.exp(-i/(ctx.sampleRate*0.018)) * 0.35;
      const g1 = ctx.createGain(); g1.gain.value = 0.55;
      const g2 = ctx.createGain(); g2.gain.value = 0.4;
      const s1 = ctx.createBufferSource(); s1.buffer = tick; s1.connect(g1); g1.connect(ctx.destination);
      const s2 = ctx.createBufferSource(); s2.buffer = thud; s2.connect(g2); g2.connect(ctx.destination);
      s1.start(0); s2.start(ctx.currentTime + 0.012);
      setTimeout(() => ctx.close(), 300);
    } catch {}
  }, [soundOn]);

  // ── Derived state ─────────────────────────────────────────────────────────
  const stats = React.useMemo(() => {
    const picks = files.filter(f => f.pick).length;
    const rejects = files.filter(f => f.reject).length;
    const rated = files.filter(f => (f.rating||0) > 0).length;
    const rated3 = files.filter(f => (f.rating||0) >= 3).length;
    const avgRating = rated ? files.reduce((s,f) => s + (f.rating||0), 0) / rated : 0;
    const unflagged = files.length - picks - rejects;
    const blurred = files.filter(f => f.blur).length;
    const unreviewed = files.filter(f => !f.pick && !f.reject && !(f.rating||0)).length;
    const uploadsGB = storageInfo ? storageInfo.uploads_size / (1024*1024*1024) : null;
    const unassigned = files.filter(f => !f.event_id).length;
    return {
      total: files.length, picks, rejects, rated, rated3, unflagged, avgRating,
      blurred, unreviewed, unassigned,
      sessionSize: uploadsGB ? `${uploadsGB.toFixed(1)} GB` : '—',
    };
  }, [files, storageInfo]);

  const tagCounts = React.useMemo(() => {
    const c = {};
    files.forEach(f => (f.tags||[]).forEach(t => { c[t] = (c[t]||0) + 1; }));
    return c;
  }, [files]);

  const visibleFiles = React.useMemo(() => {
    let f = files;
    // Event filter
    if (activeEvent === '_unassigned') f = f.filter(x => !x.event_id);
    else if (activeEvent) f = f.filter(x => x.event_id === activeEvent);
    // Smart collection filters from CollectionsDrawer
    if (activeCollection === '_picks') f = f.filter(x => x.pick);
    else if (activeCollection === '_rejected') f = f.filter(x => x.reject);
    else if (activeCollection === '_rated') f = f.filter(x => (x.rating||0) >= 3);
    else if (activeCollection === '_blur') f = f.filter(x => x.blur);
    else if (activeCollection && activeCollection.startsWith('_mb_')) {
      const token = activeCollection.slice(4);
      const mb = moodboards.find(m => m.token === token);
      const mbFiles = mb ? (mb.pins || []) : [];
      f = f.filter(x => mbFiles.includes(x.filename));
    }
    else if (activeCollection) {
      const c = collections.find(c => c.name === activeCollection);
      const cFiles = c ? (c.files || []) : [];
      f = f.filter(x => cFiles.includes(x.filename));
    }
    // Sidebar flag filter
    if (flagFilter === 'picks') f = f.filter(x => x.pick);
    else if (flagFilter === 'rejects') f = f.filter(x => x.reject);
    else if (flagFilter === 'unflagged') f = f.filter(x => !x.pick && !x.reject);
    // Toolbar quick filter
    if (toolbarFilter === 'RAW') f = f.filter(x => x.file_type === 'RAW');
    else if (toolbarFilter === 'JPG') f = f.filter(x => x.file_type === 'JPG' || x.file_type === 'JPEG');
    else if (toolbarFilter === 'picks') f = f.filter(x => x.pick);
    else if (toolbarFilter === 'rejected') f = f.filter(x => x.reject);
    else if (toolbarFilter === 'rated3') f = f.filter(x => (x.rating||0) >= 3);
    else if (toolbarFilter === 'unreviewed') f = f.filter(x => !x.pick && !x.reject && !(x.rating||0));
    // Color label filter
    if (colorFilter) f = f.filter(x => x.color === colorFilter);
    if (ratingFilter > 0) f = f.filter(x => (x.rating||0) >= ratingFilter);
    if (fileTypeFilter !== 'all') f = f.filter(x => x.file_type === fileTypeFilter);
    if (tagFilter) f = f.filter(x => (x.tags||[]).includes(tagFilter));
    if (searchQuery) {
      const q = searchQuery.toLowerCase();
      f = f.filter(x => x.filename.toLowerCase().includes(q) || (x.tags||[]).some(t => t.includes(q)));
    }
    const sorted = [...f];
    if (sort === 'capture_desc') sorted.sort((a,b) => b.timestamp - a.timestamp);
    else if (sort === 'capture_asc') sorted.sort((a,b) => a.timestamp - b.timestamp);
    else if (sort === 'rating_desc') sorted.sort((a,b) => (b.rating||0) - (a.rating||0));
    else if (sort === 'filename_asc') sorted.sort((a,b) => a.filename.localeCompare(b.filename));
    else if (sort === 'size_desc') sorted.sort((a,b) => parseFloat(b.size) - parseFloat(a.size));
    return sorted;
  }, [files, flagFilter, ratingFilter, fileTypeFilter, tagFilter, searchQuery, sort, activeCollection, collections, toolbarFilter, colorFilter, moodboards]);

  // ── File update helper (optimistic + API sync) ────────────────────────────
  const updateFile = React.useCallback((name, patch) => {
    setFiles(prev => {
      const next = prev.map(f => f.filename === name ? { ...f, ...patch } : f);
      const file = next.find(f => f.filename === name);
      if (file) {
        fetch(`/api/rate/${encodeURIComponent(name)}`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            rating: file.rating || 0,
            pick: !!file.pick,
            reject: !!file.reject,
            tags: file.tags || [],
            color: file.color || '',
          }),
        }).catch(() => addToast('Failed to save rating — check your connection'));
      }
      return next;
    });
  }, [addToast]);

  // ── Actions ───────────────────────────────────────────────────────────────
  const filesRef = React.useRef(files);
  React.useEffect(() => { filesRef.current = files; }, [files]);

  const actions = React.useMemo(() => ({
    toggleSelect: (name, on) => setSelected(s => { const n = new Set(s); if (on) n.add(name); else n.delete(name); return n; }),
    openLightbox: (idx) => setLightboxIdx(idx),
    pick: (name) => {
      const f = filesRef.current.find(f => f.filename === name);
      if (f) { updateFile(name, { pick: !f.pick, reject: false }); if (!f.pick) playShutter(); }
    },
    reject: (name) => {
      const f = filesRef.current.find(f => f.filename === name);
      if (f) updateFile(name, { reject: !f.reject, pick: false });
    },
    rate: (name, r) => {
      const f = filesRef.current.find(f => f.filename === name);
      if (f) updateFile(name, { rating: f.rating === r ? 0 : r });
    },
    color: (name, c) => {
      const f = filesRef.current.find(f => f.filename === name);
      if (f) updateFile(name, { color: f.color === c ? null : c });
    },
    tag: (name, t) => {
      const f = filesRef.current.find(f => f.filename === name);
      if (f) updateFile(name, { tags: Array.from(new Set([...(f.tags||[]), t])) });
    },
    removeTag: (name, t) => {
      const f = filesRef.current.find(f => f.filename === name);
      if (f) updateFile(name, { tags: (f.tags||[]).filter(x => x !== t) });
    },
    setTagFilter: (t) => setTagFilter(t),
  }), [updateFile, playShutter]);

  // ── Bulk actions ──────────────────────────────────────────────────────────
  const bulkApply = (patch) => {
    const names = Array.from(selected);
    setFiles(fs => fs.map(f => selected.has(f.filename) ? { ...f, ...patch } : f));
    fetch('/api/rate-bulk', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ files: names, ...patch }),
    }).catch(() => {});
  };

  // ── Auth handlers ─────────────────────────────────────────────────────────
  const handleLogin = async (username, password) => {
    const r = await fetch('/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password }),
    });
    if (r.ok) {
      const d = await r.json().catch(() => ({}));
      setAuthed(true);
      setDisplayName(d.display_name || username);
      if (d.first_login) setShowWelcome(true);
      setProfilePhotoUrl(`/api/profile/photo?t=${Date.now()}`);
      return { ok: true };
    }
    const d = await r.json().catch(() => ({}));
    return { ok: false, message: d.detail || 'Incorrect username or password.' };
  };

  const handleLogout = async () => {
    await fetch('/auth/logout', { method: 'POST' }).catch(() => {});
    setAuthed(false);
    setFiles([]);
    setCollections([]);
    setShares([]);
  };

  // ── Collection handlers ───────────────────────────────────────────────────
  const createCollection = async (name) => {
    const r = await fetch('/api/collections', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name }),
    });
    if (!r.ok) return;
    const selectedFiles = Array.from(selected);
    if (selectedFiles.length > 0) {
      await fetch(`/api/collections/${encodeURIComponent(name)}/add`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ files: selectedFiles }),
      }).catch(() => {});
    }
    setCollections(cs => [...cs, { name, files: selectedFiles, count: selectedFiles.length }]);
  };

  // ── Share handlers ────────────────────────────────────────────────────────
  const createShare = async ({ label, filter, expires }) => {
    const expDays = expires === '1d' ? 1 : expires === '7d' ? 7 : expires === '30d' ? 30 : null;
    const r = await fetch('/api/share', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ label, filter, ...(expDays ? { expires_days: expDays } : {}) }),
    });
    if (!r.ok) return null;
    const data = await r.json();
    const newShare = {
      token: data.token, label, filter, views: 0,
      expires: expDays ? Date.now() + expDays * 86400000 : null,
    };
    setShares(ss => [...ss, newShare]);
    return newShare;
  };

  const revokeShare = async (token) => {
    await fetch(`/api/share/${token}`, { method: 'DELETE' }).catch(() => {});
    setShares(ss => ss.filter(s => s.token !== token));
  };

  // ── Collection delete ─────────────────────────────────────────────────────
  const deleteCollection = async (name) => {
    await fetch(`/api/collections/${encodeURIComponent(name)}`, { method: 'DELETE' }).catch(() => {});
    setCollections(cs => cs.filter(c => c.name !== name));
    if (activeCollection === name) setActiveCollection(null);
  };

  // ── Event handlers ────────────────────────────────────────────────────────
  const createEvent = async ({ name, date, activate }) => {
    const r = await fetch('/api/events', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name, date, activate }),
    });
    if (!r.ok) return;
    const ev = await r.json();
    setEvents(es => [ev, ...es]);
    if (activate) setActiveShootId(ev.id);
  };

  const activateEvent = async (id) => {
    await fetch(`/api/events/${id || 'none'}/activate`, { method: 'POST' }).catch(() => {});
    setActiveShootId(id || null);
  };

  const deleteEvent = async (id) => {
    await fetch(`/api/events/${id}`, { method: 'DELETE' }).catch(() => {});
    setEvents(es => es.filter(e => e.id !== id));
    if (activeShootId === id) setActiveShootId(null);
    if (activeEvent === id) setActiveEvent(null);
  };

  // ── Watermark handler ─────────────────────────────────────────────────────
  const saveWatermark = async (cfg) => {
    setWatermarkCfg(cfg);
    await fetch('/api/watermark', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(cfg),
    }).catch(() => {});
  };

  // ── Delete files ──────────────────────────────────────────────────────────
  const deleteFiles = async (names) => {
    const results = await Promise.all(names.map(async name => {
      try {
        const r = await fetch(`/api/files/${encodeURIComponent(name)}`, { method: 'DELETE' });
        return { name, ok: r.ok, status: r.status };
      } catch {
        return { name, ok: false, status: 0 };
      }
    }));
    const deleted = results.filter(r => r.ok).map(r => r.name);
    const failed  = results.filter(r => !r.ok).map(r => r.name);
    if (deleted.length > 0) {
      setFiles(fs => fs.filter(f => !deleted.includes(f.filename)));
      setSelected(s => { const n = new Set(s); deleted.forEach(x => n.delete(x)); return n; });
    }
    if (failed.length > 0) {
      addToast(`Could not delete ${failed.length} file(s) — server permission error. Ask your admin to fix upload directory permissions.`);
    }
  };

  // ── EXIF lazy loading ─────────────────────────────────────────────────────
  const loadExif = React.useCallback((filename) => {
    if (exifCache[filename] !== undefined) return;
    setExifCache(c => ({ ...c, [filename]: null }));
    fetch(`/api/exif/${encodeURIComponent(filename)}`)
      .then(r => r.ok ? r.json() : null)
      .then(data => setExifCache(c => ({ ...c, [filename]: data || {} })))
      .catch(() => setExifCache(c => ({ ...c, [filename]: {} })));
  }, [exifCache]);

  // ── Keyboard shortcuts ────────────────────────────────────────────────────
  React.useEffect(() => {
    const gridCols = density === 'tight' ? 6 : density === 'loose' ? 3 : 4;
    const handler = (e) => {
      if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return;
      if (lightboxIdx !== null) return;
      const file = visibleFiles[focused];
      if (e.key === 'Enter' && file) { setLightboxIdx(focused); e.preventDefault(); }
      if ((e.key === 'p' || e.key === 'P') && file) { actions.pick(file.filename); e.preventDefault(); }
      if ((e.key === 'x' || e.key === 'X') && file) { actions.reject(file.filename); e.preventDefault(); }
      if (e.key >= '1' && e.key <= '5' && file) { actions.rate(file.filename, parseInt(e.key)); e.preventDefault(); }
      if (e.key === '0' && file) { actions.rate(file.filename, 0); e.preventDefault(); }
      if (e.key === 'ArrowRight') { setFocused(i => Math.min(i + 1, visibleFiles.length - 1)); e.preventDefault(); }
      if (e.key === 'ArrowLeft') { setFocused(i => Math.max(0, i - 1)); e.preventDefault(); }
      if (e.key === 'ArrowDown') { setFocused(i => Math.min(i + gridCols, visibleFiles.length - 1)); e.preventDefault(); }
      if (e.key === 'ArrowUp') { setFocused(i => Math.max(0, i - gridCols)); e.preventDefault(); }
      if ((e.metaKey || e.ctrlKey) && e.key === 'a') { setSelected(new Set(visibleFiles.map(f => f.filename))); e.preventDefault(); }
      if (e.key === 'Escape') setSelected(new Set());
      if (e.key === '?') setShortcutsOpen(true);
      if (e.key === 'g') setView('grid');
      if (e.key === 'l') setView('list');
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [visibleFiles, focused, lightboxIdx, actions, density]);

  // ── Broadcast to second screen ────────────────────────────────────────────
  // On lightbox nav — immediate
  React.useEffect(() => {
    if (lightboxIdx === null) return;
    const file = visibleFiles[lightboxIdx];
    if (file) broadcastPhoto(file);
  }, [lightboxIdx]); // eslint-disable-line

  // On grid focus change — debounced so fast keyboard nav doesn't flood
  React.useEffect(() => {
    const t = setTimeout(() => {
      if (focused == null || lightboxIdx !== null) return;
      const file = visibleFiles[focused];
      if (file) broadcastPhoto(file);
    }, 280);
    return () => clearTimeout(t);
  }, [focused]); // eslint-disable-line

  // ── Load saved edits from server when opening lightbox ───────────────────
  React.useEffect(() => {
    if (lightboxIdx === null) return;
    const file = visibleFiles[lightboxIdx];
    if (!file) return;
    // Only fetch if we have no local edits stored for this file yet
    if (edits[file.filename] !== undefined) return;
    fetch(`/api/edit/${encodeURIComponent(file.filename)}`)
      .then(r => r.ok ? r.json() : null)
      .then(data => {
        if (data) setEdits(e => ({ ...e, [file.filename]: data }));
      })
      .catch(() => {});
  }, [lightboxIdx]); // eslint-disable-line react-hooks/exhaustive-deps

  // ── Loading state ─────────────────────────────────────────────────────────
  if (authed === null) {
    return (
      <div style={{ position: 'fixed', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--bg-0)' }}>
        <div className="mono" style={{ color: 'var(--ink-3)', fontSize: 11, letterSpacing: 0.1 }}>LOADING…</div>
      </div>
    );
  }

  // ── Public share view (route /share/:token) — no auth required ──────────
  const shareToken = window.location.pathname.match(/^\/share\/([^/]+)/)?.[1];
  if (shareToken) {
    return <PublicShareView token={shareToken}/>;
  }

  // ── Login gate ────────────────────────────────────────────────────────────
  if (!authed) return <LoginScreen onLogin={handleLogin} theme={theme} setTheme={setTheme}/>;

  // ── Internal share preview ────────────────────────────────────────────────
  if (shareViewOpen) {
    return (
      <ShareView
        files={files.filter(f => f.pick)}
        share={{ label: 'Live Session — Picks preview' }}
        onOpen={(i) => { setShareViewOpen(false); setLightboxIdx(i); }}
        onClose={() => setShareViewOpen(false)}
        onFeedback={(name) => setFeedback(s => { const n = new Set(s); if (n.has(name)) n.delete(name); else n.add(name); return n; })}
        feedback={feedback}
      />
    );
  }

  const currentFile = lightboxIdx !== null ? visibleFiles[lightboxIdx] : null;
  const currentEdits = currentFile ? (edits[currentFile.filename] || {}) : {};

  if (currentFile && !exifCache[currentFile.filename] && exifCache[currentFile.filename] !== null) {
    loadExif(currentFile.filename);
  }

  // ── Main dashboard ────────────────────────────────────────────────────────
  return (
    <div style={{ display: 'flex', height: '100vh', overflow: 'hidden', background: 'var(--bg-0)' }}>
      <CollectionsDrawer
        open={collectionsDrawerOpen}
        collections={collections}
        activeCollection={activeCollection}
        setActiveCollection={setActiveCollection}
        files={files}
        onClose={() => setCollectionsDrawerOpen(false)}
        onNew={() => { setCollectionsDrawerOpen(false); setNewCollectionOpen(true); }}
        moodboards={moodboards}
      />

      <Sidebar
        open={sidebarOpen} setOpen={setSidebarOpen}
        events={events} activeEvent={activeEvent} setActiveEvent={setActiveEvent}
        activeShootId={activeShootId}
        onCreateEvent={() => setEventModalOpen(true)}
        onActivateEvent={activateEvent}
        onDeleteEvent={deleteEvent}
        collections={collections}
        onDeleteCollection={deleteCollection}
        activeCollection={activeCollection} setActiveCollection={setActiveCollection}
        tagFilter={tagFilter} setTagFilter={setTagFilter}
        ratingFilter={ratingFilter} setRatingFilter={setRatingFilter}
        flagFilter={flagFilter} setFlagFilter={setFlagFilter}
        fileTypeFilter={fileTypeFilter} setFileTypeFilter={setFileTypeFilter}
        tagCounts={tagCounts} stats={stats}
        onNewCollection={() => setNewCollectionOpen(true)}
        shareCount={shares.length}
        onOpenShares={() => setShareOpen(true)}
        onOpenWatermark={() => setWatermarkOpen(true)}
        onOpenShortcuts={() => setShortcutsOpen(true)}
        onOpenBranding={() => setShowBranding(true)}
        displayName={displayName}
        profilePhotoUrl={profilePhotoUrl}
        onOpenProfile={() => setShowProfilePhoto(true)}
        onLogout={handleLogout}
      />

      <main style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minWidth: 0 }}>
        <Topbar
          view={view} setView={setView}
          sort={sort} setSort={setSort}
          density={density} setDensity={setDensity}
          selected={selected}
          onSelectAll={() => setSelected(new Set(visibleFiles.map(f => f.filename)))}
          onClearSelect={() => setSelected(new Set())}
          onBulkPick={() => bulkApply({ pick: true, reject: false })}
          onBulkReject={() => bulkApply({ reject: true, pick: false })}
          onBulkShare={() => setShareOpen(true)}
          onBulkDownload={() => {
            const names = Array.from(selected);
            fetch('/api/download-zip', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ files: names }),
            })
              .then(r => r.ok ? r.blob() : null)
              .then(blob => {
                if (!blob) return;
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url; a.download = 'gallery_selection.zip'; a.click();
                URL.revokeObjectURL(url);
              })
              .catch(() => {});
          }}
          onBulkDelete={() => deleteFiles(Array.from(selected))}
          onSearch={setSearchQuery} searchQuery={searchQuery}
          stats={stats}
          capturing={capturing} lastCapture={lastCapture}
          analyticsOpen={analyticsOpen} setAnalyticsOpen={setAnalyticsOpen}
          onToggleSidebar={() => setSidebarOpen(o => !o)}
          onOpenCollections={() => setCollectionsDrawerOpen(true)}
          onShare={() => setShareOpen(true)}
          onShareView={() => setShareViewOpen(true)}
          onOpenShortcuts={() => setShortcutsOpen(true)}
          onSecondScreen={openSecondScreen}
        />

        <AnalyticsBar stats={stats} open={analyticsOpen}/>

        <Toolbar
          filter={toolbarFilter} setFilter={setToolbarFilter}
          sort={sort} setSort={setSort}
          search={searchQuery} setSearch={setSearchQuery}
          selCount={selected.size}
          onDlSel={() => {
            const names = Array.from(selected);
            fetch('/api/download-zip', {
              method: 'POST', headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ files: names }),
            }).then(r => r.ok ? r.blob() : null).then(blob => {
              if (!blob) return;
              const url = URL.createObjectURL(blob);
              const a = document.createElement('a');
              a.href = url; a.download = 'gallery_selection.zip'; a.click();
              URL.revokeObjectURL(url);
            }).catch(() => {});
          }}
          onDelSel={() => deleteFiles(Array.from(selected))}
          totalShown={visibleFiles.length}
          activeTag={tagFilter} setActiveTag={setTagFilter}
          colorFilter={colorFilter} setColorFilter={setColorFilter}
        />

        {/* Active filter chips — sidebar filters still shown as chips */}
        {(flagFilter !== 'all' || ratingFilter > 0 || fileTypeFilter !== 'all' || activeCollection) && (
          <div style={{
            padding: '5px 16px', display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap',
            background: 'var(--bg-0)', borderBottom: '1px solid var(--line-1)', fontSize: 11, flexShrink: 0,
          }}>
            <span className="mono" style={{ color: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: 0.1, fontSize: 9 }}>Sidebar filters</span>
            {activeCollection && <FilterChip onRemove={() => setActiveCollection(null)}>
              {activeCollection === '_picks' ? 'Picks' : activeCollection === '_rejected' ? 'Rejected' : activeCollection === '_rated' ? 'Rated 3★+' : activeCollection === '_blur' ? 'Soft focus' : `Collection: ${activeCollection}`}
            </FilterChip>}
            {flagFilter !== 'all' && <FilterChip onRemove={() => setFlagFilter('all')}>{flagFilter}</FilterChip>}
            {ratingFilter > 0 && <FilterChip onRemove={() => setRatingFilter(0)}>≥ {ratingFilter}★</FilterChip>}
            {fileTypeFilter !== 'all' && <FilterChip onRemove={() => setFileTypeFilter('all')}>{fileTypeFilter}</FilterChip>}
          </div>
        )}

        {/* Gallery area */}
        <div style={{ flex: 1, overflowY: 'auto', position: 'relative' }} onClick={() => setSelected(new Set())}>
          <div onClick={e => e.stopPropagation()}>
            {loading ? (
              <div style={{ padding: 80, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
                <div style={{ width: 32, height: 32, borderRadius: '50%', border: '2px solid var(--line-2)', borderTopColor: 'var(--accent)', animation: 'spin 0.8s linear infinite' }}/>
                <div className="mono" style={{ color: 'var(--ink-3)', fontSize: 11, letterSpacing: 0.1 }}>Loading gallery…</div>
              </div>
            ) : visibleFiles.length === 0 ? (
              <EmptyState clearAll={() => { setTagFilter(null); setRatingFilter(0); setFlagFilter('all'); setFileTypeFilter('all'); setActiveCollection(null); setSearchQuery(''); }}/>
            ) : view === 'grid' ? (
              <GridView items={visibleFiles} focused={focused} setFocused={setFocused} selected={selected} actions={actions} density={density}/>
            ) : (
              <ListView items={visibleFiles} focused={focused} setFocused={setFocused} selected={selected} actions={actions}/>
            )}
          </div>
        </div>

        {/* Status bar — desktop only */}
        <div style={{
          height: 22, flexShrink: 0, background: 'var(--bg-1)', borderTop: '1px solid var(--line-1)',
          padding: '0 14px', display: window.innerWidth < 768 ? 'none' : 'flex', alignItems: 'center', gap: 14,
          fontFamily: 'var(--mono)', fontSize: 10, color: 'var(--ink-3)', letterSpacing: 0.04,
        }}>
          <span>{visibleFiles.length} / {files.length} files</span>
          {storageInfo && (() => {
            const usedMB = (storageInfo.uploads_size / (1024*1024)).toFixed(0);
            const quotaMB = storageInfo.quota_mb;
            if (quotaMB) {
              const pct = Math.min(100, Math.round((storageInfo.uploads_size / (quotaMB * 1024 * 1024)) * 100));
              return <>
                <span>·</span>
                <span style={{ color: pct > 90 ? 'var(--reject)' : pct > 70 ? '#ffb27a' : 'var(--ink-3)' }}>
                  {usedMB} MB / {quotaMB} MB
                </span>
                <span style={{ width: 48, height: 3, background: 'var(--bg-3)', borderRadius: 2, overflow: 'hidden', display: 'inline-block', verticalAlign: 'middle' }}>
                  <span style={{ display: 'block', height: '100%', width: `${pct}%`, background: pct > 90 ? 'var(--reject)' : pct > 70 ? '#ffb27a' : 'var(--accent)', borderRadius: 2 }}/>
                </span>
              </>;
            }
            return <><span>·</span><span>{usedMB} MB used</span></>;
          })()}
          <div style={{ flex: 1 }}/>
          <span>Auto-saved</span>
          <span>·</span>
          <button onClick={() => setTheme(t => t === 'graphite' ? 'darkroom' : t === 'darkroom' ? 'blanc' : 'graphite')}
            style={{ background: 'none', border: 'none', color: 'var(--ink-3)', fontFamily: 'var(--mono)', fontSize: 10, cursor: 'pointer', textTransform: 'uppercase', letterSpacing: 0.1 }}>
            {theme}
          </button>
          <span>·</span>
          <button onClick={() => { const next = !soundOn; setSoundOn(next); localStorage.setItem('studio.sound', next ? 'on' : 'off'); }}
            title={soundOn ? 'Sound on — click to mute' : 'Sound off — click to enable'}
            style={{ background: 'none', border: 'none', color: soundOn ? 'var(--accent)' : 'var(--ink-4)', fontFamily: 'var(--mono)', fontSize: 10, cursor: 'pointer', letterSpacing: 0.1 }}>
            {soundOn ? '♪ SND' : '♪ mute'}
          </button>
          <span>·</span>
          <button onClick={openSecondScreen}
            title="Open second screen monitor"
            style={{ background: 'none', border: 'none', color: 'var(--ink-3)', fontFamily: 'var(--mono)', fontSize: 10, cursor: 'pointer', letterSpacing: 0.1 }}>
            2ND SCR
          </button>
          <span>·</span>
          <button onClick={handleLogout}
            style={{ background: 'none', border: 'none', color: 'var(--ink-3)', fontFamily: 'var(--mono)', fontSize: 10, cursor: 'pointer' }}>
            Logout
          </button>
        </div>

        <ShortcutBar/>
      </main>

      {/* Lightbox */}
      {currentFile && (
        <Lightbox
          items={visibleFiles} index={lightboxIdx}
          exifCache={exifCache}
          onClose={() => { setLightboxIdx(null); setZoom(false); setBaMode(false); setDevOpen(false); setShowExif(false); }}
          onNav={(d) => setLightboxIdx(i => Math.max(0, Math.min(visibleFiles.length - 1, i + d)))}
          onPick={() => actions.pick(currentFile.filename)}
          onReject={() => actions.reject(currentFile.filename)}
          onRate={(r) => actions.rate(currentFile.filename, r)}
          onColor={(c) => actions.color(currentFile.filename, c)}
          devOpen={devOpen} setDevOpen={setDevOpen}
          edits={currentEdits}
          onEditChange={(k, v) => setEdits(e => ({ ...e, [currentFile.filename]: { ...(e[currentFile.filename]||{}), [k]: v }}))}
          onEditReset={(k) => setEdits(e => ({ ...e, [currentFile.filename]: { ...(e[currentFile.filename]||{}), [k]: 0 }}))}
          showExif={showExif} setShowExif={setShowExif}
          baMode={baMode} setBaMode={setBaMode}
          zoom={zoom} setZoom={setZoom}
        />
      )}

      {/* Develop panel */}
      {currentFile && (
        <DevelopPanel
          open={devOpen} setOpen={setDevOpen}
          filename={currentFile.filename}
          edits={currentEdits}
          onChange={(k, v) => setEdits(e => ({ ...e, [currentFile.filename]: { ...(e[currentFile.filename]||{}), [k]: v }}))}
          onReset={(k) => setEdits(e => ({ ...e, [currentFile.filename]: { ...(e[currentFile.filename]||{}), [k]: 0 }}))}
          onResetAll={() => {
            setEdits(e => ({ ...e, [currentFile.filename]: {} }));
            fetch(`/api/edit/${encodeURIComponent(currentFile.filename)}/reset`, { method: 'POST' }).catch(() => {});
          }}
          onSave={() => fetch(`/api/edit/${encodeURIComponent(currentFile.filename)}`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(currentEdits),
          }).catch(() => {})}
          onApplyToAll={() => {}}
        />
      )}

      {/* Modals */}
      {shareOpen && (
        <ShareModal
          onClose={() => setShareOpen(false)}
          shares={shares}
          collections={collections}
          onCreate={createShare}
          onRevoke={revokeShare}
        />
      )}
      {watermarkOpen && (
        <WatermarkModal
          onClose={() => setWatermarkOpen(false)}
          config={watermarkCfg}
          onSave={saveWatermark}
        />
      )}
      {newCollectionOpen && (
        <NewCollectionModal
          onClose={() => setNewCollectionOpen(false)}
          onCreate={createCollection}
        />
      )}
      {shortcutsOpen && <ShortcutsModal onClose={() => setShortcutsOpen(false)}/>}
      {eventModalOpen && (
        <EventModal
          onClose={() => setEventModalOpen(false)}
          onCreate={async (data) => { await createEvent(data); setEventModalOpen(false); }}
        />
      )}

      {/* Welcome / onboarding */}
      {showWelcome && <WelcomeModal displayName={displayName} onClose={() => setShowWelcome(false)}/>}

      {/* Branding */}
      {showBranding && <BrandingModal onClose={() => setShowBranding(false)}/>}

      {/* Profile photo */}
      {showProfilePhoto && <ProfilePhotoModal
        onClose={() => setShowProfilePhoto(false)}
        onSaved={() => setProfilePhotoUrl(`/api/profile/photo?t=${Date.now()}`)}
      />}

      {/* Toast notifications */}
      {toasts.length > 0 && (
        <div style={{
          position: 'fixed', bottom: 40, left: '50%', transform: 'translateX(-50%)',
          display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8, zIndex: 9999,
          pointerEvents: 'none',
        }}>
          {toasts.map(t => (
            <div key={t.id} style={{
              padding: '9px 16px', borderRadius: 6, fontSize: 12.5,
              background: t.type === 'error' ? 'rgba(220,60,60,0.92)' : 'rgba(30,160,100,0.92)',
              color: '#fff', boxShadow: '0 4px 16px rgba(0,0,0,0.4)',
              backdropFilter: 'blur(8px)', letterSpacing: 0.02,
              animation: 'fade-up 180ms ease',
            }}>{t.msg}</div>
          ))}
        </div>
      )}
    </div>
  );
}

// ── Filter chip ───────────────────────────────────────────────────────────────
function FilterChip({ children, onRemove }) {
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center', gap: 4,
      padding: '2px 4px 2px 8px', fontSize: 10.5,
      background: 'var(--bg-2)', border: '1px solid var(--line-1)',
      borderRadius: 3, color: 'var(--ink-1)',
    }}>
      {children}
      <button onClick={onRemove} style={{
        background: 'transparent', border: 'none', color: 'var(--ink-3)',
        cursor: 'pointer', padding: 2, display: 'flex',
      }}><IconX size={9}/></button>
    </span>
  );
}

// ── Empty state ───────────────────────────────────────────────────────────────
function EmptyState({ clearAll }) {
  return (
    <div style={{
      padding: 60, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
      minHeight: 400, color: 'var(--ink-3)', textAlign: 'center', gap: 12,
    }}>
      <div style={{ width: 48, height: 48, borderRadius: '50%', background: 'var(--bg-2)', border: '1px solid var(--line-1)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
        <IconSearch size={18}/>
      </div>
      <div className="serif" style={{ fontSize: 22, color: 'var(--ink-1)', letterSpacing: -0.3 }}>No photos match</div>
      <div style={{ fontSize: 12.5, color: 'var(--ink-3)', maxWidth: 280 }}>Adjust your filters or clear them to see everything.</div>
      <Btn variant="outline" onClick={clearAll} style={{ marginTop: 6 }}>Clear all filters</Btn>
    </div>
  );
}

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