// Shot List editor — the design's app.jsx, wired to per-project localStorage.
// Loaded after icons / data / chrome / cards / table / data-input have
// registered globals (Rail, Header, ProjectSubheader, ProjectDetailsModal,
// SceneGroup, FlatShotsGrid, ShotTable, DataInputModal, BUILTIN_FIELDS, …).
//
// Exports `window.ShotListApp` so the root entry can render it.
const { useState: useStateApp, useEffect: useEffectApp, useMemo: useMemoApp, useRef: useRefApp } = React;

// Default config for a brand-new project (matches the design's defaults).
const DEFAULT_DATA_CONFIG = {
  enabled: { shot: true, camera: false, lens: true, move: true, duration: true, location: false },
  custom: [],
  order: [],
};
const DEFAULT_PROJECT = {
  title: 'Untitled Shot List',
  kind: '',                       // empty → modal shows the "Commercial" placeholder
  dates: '',
  crew: { director: '', dop: '', production: '', agency: '' },
  formats: [],
};

// ──────────────── Migration: legacy flat-shots format → scene-based ─────────
// Old v1.0 / v1.1 per-project state had a flat shots[] at the top level and
// no project / dataConfig. Detect and convert so existing projects keep working.
function migrateLegacyState(old) {
  if (!old || typeof old !== 'object') return null;
  if (Array.isArray(old.scenes)) return old;       // already migrated
  if (!Array.isArray(old.shots)) return null;      // unknown shape
  const statusMap = { pending: 'draft', inprogress: 'review', done: 'approved' };
  const newShots = old.shots.map(s => ({
    id: s.id || ('s' + Math.random().toString(36).slice(2,8)),
    title: '',                              // old format had no separate title
    desc: s.description || '',
    shot: s.shotSize || '',
    camera: s.camera || '',
    lens: s.lens || '',
    move: s.movement || '',
    duration: '',
    location: '',
    status: statusMap[s.status] || 'draft',
    img: s.image || '',
    custom: {},
  }));
  return {
    view: 'cards',
    grouping: 'scenes',
    ar: '16-9',
    density: 'full',
    project: { ...DEFAULT_PROJECT, title: old.title || 'Untitled Shot List' },
    dataConfig: { ...DEFAULT_DATA_CONFIG },
    scenes: [{
      id: 'sc-1',
      number: '01',
      title: 'Scene 1',
      location: '',
      timeOfDay: '',
      shots: newShots,
    }],
  };
}

// First-time seed for a brand-new project: a clean slate, not the demo. One
// scene with three shots — the first carries a worked example (Close-Up /
// 35mm / slow dolly back / 2s) to show the shape of a filled-in shot; the
// other two are blank. No images, no example crew/format.
function buildSeedState(meta) {
  const blankShot = () => ({
    id: 's' + Math.random().toString(36).slice(2, 8),
    title: '', desc: '',
    shot: '', camera: '', lens: '', move: '', duration: '', location: '',
    status: 'draft', img: '', custom: {},
  });
  const firstShot = {
    ...blankShot(),
    shot: 'Close-Up',          // matches a SHOT_SIZES preset (so the dropdown shows it)
    lens: '35mm',
    move: 'Slow dolly back',
    duration: '2s',
  };
  return {
    view: 'cards',
    grouping: 'scenes',
    ar: '16-9',
    density: 'full',
    project: { ...DEFAULT_PROJECT, title: (meta && meta.title) || DEFAULT_PROJECT.title },
    dataConfig: { ...DEFAULT_DATA_CONFIG },
    scenes: [{
      id: 'sc-1',
      number: '01',
      title: 'Scene 1',
      location: '',
      timeOfDay: '',
      shots: [firstShot, blankShot(), blankShot()],
    }],
  };
}

function loadProjectState(meta) {
  if (!meta) return buildSeedState(meta);
  let raw;
  try { raw = localStorage.getItem(window.projectKey(meta.id)); } catch (e) {}
  if (!raw) return buildSeedState(meta);
  try {
    const data = JSON.parse(raw);
    const migrated = migrateLegacyState(data);
    if (migrated) {
      // Ensure project.title mirrors whatever is in the index entry (source of truth).
      migrated.project = { ...DEFAULT_PROJECT, ...migrated.project, title: meta.title || migrated.project.title };
      // Backfill missing dataConfig fields
      migrated.dataConfig = { ...DEFAULT_DATA_CONFIG, ...(migrated.dataConfig || {}) };
      return migrated;
    }
  } catch (e) { console.error('loadProjectState parse failed', e); }
  return buildSeedState(meta);
}

function ShotListApp({ meta }) {
  const [theme, setTheme] = useStateApp(() => localStorage.getItem('pp-theme') || 'dark');

  // Hydrate the full state from localStorage (or seed) — only once.
  const initial = useMemoApp(() => loadProjectState(meta), [meta && meta.id]);

  const [view, setView] = useStateApp(initial.view);
  const [grouping, setGrouping] = useStateApp(initial.grouping);
  const [ar, setAr] = useStateApp(initial.ar);
  const [density, setDensity] = useStateApp(initial.density);
  const [collapsed, setCollapsed] = useStateApp({});
  const [scenes, setScenes] = useStateApp(initial.scenes);
  const [project, setProject] = useStateApp(initial.project);
  const [dataConfig, setDataConfig] = useStateApp(initial.dataConfig);
  const [showProjectModal, setShowProjectModal] = useStateApp(false);
  const [showDataModal, setShowDataModal] = useStateApp(false);
  const [showReorderModal, setShowReorderModal] = useStateApp(false);
  const [showShareModal, setShowShareModal] = useStateApp(false);
  const [commentCounts, setCommentCounts] = useStateApp({});   // shotId → { total, unresolved }
  const [galleryIndex, setGalleryIndex] = useStateApp(null);    // flat index of shot to open, or null
  const [undoStack, setUndoStack] = useStateApp([]);
  const [toast, setToast] = useStateApp(null);

  // ── data-durability UI state ──
  const [saveError, setSaveError] = useStateApp(null);   // null | { quota }
  const [signedIn, setSignedIn] = useStateApp(null);     // null = unknown yet
  const [conflict, setConflict] = useStateApp(false);    // another device changed this list
  const [showHistory, setShowHistory] = useStateApp(false);
  const [offlineDismissed, setOfflineDismissed] = useStateApp(() => {
    try {
      const ts = parseInt(localStorage.getItem('pp-offline-nudge-dismissed') || '0', 10);
      return !!ts && (Date.now() - ts) < 7 * 24 * 60 * 60 * 1000;
    } catch (e) { return false; }
  });
  const baselineRemote = useRefApp(0);

  const pushUndo = (entry) => setUndoStack(s => [...s.slice(-19), entry]);

  // ──────────────── First-run prompt ────────────────
  // A project created from the Projects page arrives with ?new=1. Pop the
  // Project Details modal right away so the user fills in metadata first,
  // then strip the flag so a reload (or back/forward) won't re-trigger it.
  useEffectApp(() => {
    const params = new URLSearchParams(location.search);
    if (params.get('new') === '1') {
      setShowProjectModal(true);
      params.delete('new');
      const qs = params.toString();
      history.replaceState(null, '', location.pathname + (qs ? '?' + qs : ''));
    }
  }, []);

  // ──────────────── Persist on every change (debounced) ────────────────
  const saveTimer = useRefApp(null);
  useEffectApp(() => {
    if (!meta) return;
    clearTimeout(saveTimer.current);
    saveTimer.current = setTimeout(() => {
      const snapshot = { view, grouping, ar, density, scenes, project, dataConfig };
      const ok = window.safeSetItem(window.projectKey(meta.id), JSON.stringify(snapshot));
      if (ok) {
        setSaveError(null);
        // Update the project metadata index so the projects page reflects
        // the new title, shot count, and modified time.
        const shotCount = scenes.reduce((n, sc) => n + sc.shots.length, 0);
        window.updateProjectMeta(meta.id, {
          title: project.title || 'Untitled Shot List',
          shotCount,
        });
      }
      // On failure, safeSetItem fired `pp-save-error` → the warning banner shows.
    }, 350);
    return () => clearTimeout(saveTimer.current);
  }, [meta, view, grouping, ar, density, scenes, project, dataConfig]);

  // ──────────────── Data-durability safeguards ────────────────
  // 1) Surface save failures (storage full, etc.) as a persistent banner.
  useEffectApp(() => {
    const onErr = (e) => setSaveError({ quota: !!(e.detail && e.detail.quota) });
    window.addEventListener('pp-save-error', onErr);
    return () => window.removeEventListener('pp-save-error', onErr);
  }, []);

  // 2) Track sign-in state (drives the "back up to the cloud" nudge).
  useEffectApp(() => {
    const sb = window.supabaseClient;
    if (!sb) { setSignedIn(false); return; }
    let mounted = true;
    sb.auth.getSession().then(({ data }) => { if (mounted) setSignedIn(!!(data && data.session)); });
    const { data: sub } = sb.auth.onAuthStateChange((_e, session) => { if (mounted) setSignedIn(!!session); });
    return () => { mounted = false; if (sub && sub.subscription) sub.subscription.unsubscribe(); };
  }, []);
  const dismissOffline = () => {
    setOfflineDismissed(true);
    try { localStorage.setItem('pp-offline-nudge-dismissed', String(Date.now())); } catch (e) {}
  };

  // 3) Concurrent-edit detection: baseline the server's updated_at, then on
  // focus check whether another device wrote since (our own pushes advance
  // window.ppLastPushedAt, so they don't false-trigger).
  useEffectApp(() => {
    if (!meta || signedIn !== true || typeof window.ppGetRemoteUpdatedAt !== 'function') return;
    let alive = true;
    window.ppGetRemoteUpdatedAt(meta.id).then((t) => { if (alive && t) baselineRemote.current = t; });
    return () => { alive = false; };
  }, [meta && meta.id, signedIn]);
  useEffectApp(() => {
    const check = async () => {
      if (document.visibilityState !== 'visible') return;
      if (!meta || signedIn !== true || typeof window.ppGetRemoteUpdatedAt !== 'function') return;
      const remote = await window.ppGetRemoteUpdatedAt(meta.id);
      if (!remote) return;
      const mine = (window.ppLastPushedAt && window.ppLastPushedAt[meta.id]) || baselineRemote.current;
      if (remote > mine + 1500) { baselineRemote.current = remote; setConflict(true); }
    };
    document.addEventListener('visibilitychange', check);
    window.addEventListener('focus', check);
    return () => { document.removeEventListener('visibilitychange', check); window.removeEventListener('focus', check); };
  }, [meta && meta.id, signedIn]);

  // 4) Restore a cloud snapshot into the editor (flows through the normal save;
  // the DB trigger snapshots the pre-restore state first, so it's reversible).
  const restoreVersion = async (versionId) => {
    if (typeof window.ppGetVersion !== 'function') return;
    const st = await window.ppGetVersion(versionId);
    if (!st) { setToast('Could not load that version'); setTimeout(() => setToast(null), 2200); return; }
    if (Array.isArray(st.scenes)) setScenes(st.scenes);
    if (st.project) setProject(st.project);
    if (st.dataConfig) setDataConfig(st.dataConfig);
    if (st.view) setView(st.view);
    if (st.grouping) setGrouping(st.grouping);
    if (st.ar) setAr(st.ar);
    if (st.density) setDensity(st.density);
    setShowHistory(false);
    setToast('Restored a previous version');
    setTimeout(() => setToast(null), 2400);
  };

  // ──────────────── Undo (Cmd/Ctrl+Z) ────────────────
  useEffectApp(() => {
    const onKey = (e) => {
      const isUndo = (e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === 'z';
      if (!isUndo) return;
      const t = e.target;
      const inField = t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable);
      if (inField && t.value && t.value.length > 0) return;
      e.preventDefault();
      setUndoStack(s => {
        if (!s.length) return s;
        const last = s[s.length - 1];
        if (last.type === 'deleteScene') {
          setScenes(prev => {
            const next = [...prev];
            next.splice(Math.min(last.index, next.length), 0, last.scene);
            return next;
          });
          setToast(`Restored scene "${last.scene.title || 'Untitled'}"`);
          setTimeout(() => setToast(null), 2200);
        } else if (last.type === 'deleteShot') {
          setScenes(prev => prev.map(sc => {
            if (sc.id !== last.sceneId) return sc;
            const next = [...sc.shots];
            next.splice(Math.min(last.index, next.length), 0, last.shot);
            return { ...sc, shots: next };
          }));
          setToast(`Restored shot "${last.shot.title || 'Untitled'}"`);
          setTimeout(() => setToast(null), 2200);
        }
        return s.slice(0, -1);
      });
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);

  const deleteScene = (sceneId) => {
    setScenes(prev => {
      const idx = prev.findIndex(s => s.id === sceneId);
      if (idx === -1) return prev;
      pushUndo({ type: 'deleteScene', scene: prev[idx], index: idx });
      setToast('Scene deleted · ⌘Z to undo');
      setTimeout(() => setToast(null), 2400);
      return prev.filter(s => s.id !== sceneId);
    });
  };

  useEffectApp(() => {
    document.documentElement.dataset.theme = theme;
    localStorage.setItem('pp-theme', theme);
  }, [theme]);

  // ──────────────── Owner comment inbox (per-card counts + gallery) ────────────────
  // Load the project's client comments (owner-scoped RPC) so each card can show
  // a count badge. Signed-out / unsynced projects just show no counts.
  const refreshCommentCounts = async () => {
    const sb = window.supabaseClient;
    if (!sb || !meta) return;
    try {
      const { data: sess } = await sb.auth.getSession();
      if (!sess || !sess.session) return;
      const { data: rows, error } = await sb.rpc('review_owner_list_comments', { p_shot_list_id: meta.id });
      if (error) return;
      const m = {};
      (rows || []).forEach(c => {
        const e = m[c.shot_id] || (m[c.shot_id] = { total: 0, unresolved: 0 });
        e.total += 1;
        if (!c.resolved) e.unresolved += 1;
      });
      setCommentCounts(m);
    } catch (e) { /* no counts */ }
  };
  useEffectApp(() => { refreshCommentCounts(); }, [meta && meta.id]);

  // Open the reviewer-style gallery at a given shot so the owner can read +
  // reply to comments. Requires sign-in (the comment RPCs are authed).
  const openComments = async (shotId) => {
    const sb = window.supabaseClient;
    if (sb) {
      try {
        const { data: sess } = await sb.auth.getSession();
        if (!sess || !sess.session) { if (window.__jumpOpenAuth) window.__jumpOpenAuth(); return; }
      } catch (e) {}
    }
    let n = -1, idx = -1;
    for (const sc of scenes) {
      for (const sh of (sc.shots || [])) {
        n += 1;
        if (sh.id === shotId) { idx = n; break; }
      }
      if (idx >= 0) break;
    }
    setGalleryIndex(idx < 0 ? 0 : idx);
  };
  const CommentGallery = window.CommentGallery;

  const allShots = scenes.flatMap(s => s.shots);
  // Entitlement (Free vs Solo). Defaults to Solo if billing.jsx hasn't loaded.
  const ent = (typeof window.useEntitlement === 'function') ? window.useEntitlement() : { solo: true };
  const FREE_FRAME_LIMIT = 60;
  // Returns true (and prompts upgrade) when the free per-storyboard frame cap is hit.
  const guardFrameLimit = () => {
    if (!ent.solo && allShots.length >= FREE_FRAME_LIMIT) {
      if (window.ppOpenUpgrade) window.ppOpenUpgrade(`Free storyboards are capped at ${FREE_FRAME_LIMIT} frames. Upgrade to Solo for unlimited frames.`);
      return true;
    }
    return false;
  };
  const approved = allShots.filter(s => s.status === 'approved').length;

  const updateShot = (shotId, patch) => {
    setScenes(prev => prev.map(sc => ({
      ...sc,
      shots: sc.shots.map(sh => sh.id === shotId ? { ...sh, ...patch } : sh)
    })));
  };

  const changeStatus = (shotId, status) => updateShot(shotId, { status });

  const updateScene = (sceneId, patch) => {
    setScenes(prev => prev.map(sc => sc.id === sceneId ? { ...sc, ...patch } : sc));
  };

  const addScene = () => {
    setScenes(prev => [...prev, {
      id: 'sc' + Math.random().toString(36).slice(2,8),
      number: String(prev.length + 1).padStart(2, '0'),
      title: 'New Scene',
      location: '',
      timeOfDay: '',
      shots: [],
    }]);
  };

  const addShot = (sceneId) => {
    if (guardFrameLimit()) return;
    setScenes(prev => prev.map(sc => {
      if (sc.id !== sceneId) return sc;
      const newShot = {
        id: 's' + Math.random().toString(36).slice(2,8),
        title: '', desc: '', shot: '', camera: '', lens: '', move: '', duration: '', location: '',
        status: 'draft', img: '', custom: {}
      };
      return { ...sc, shots: [...sc.shots, newShot] };
    }));
  };

  const deleteShot = (shotId) => {
    setScenes(prev => {
      let captured = null;
      const next = prev.map(sc => {
        const idx = sc.shots.findIndex(sh => sh.id === shotId);
        if (idx === -1) return sc;
        captured = { sceneId: sc.id, index: idx, shot: sc.shots[idx] };
        return { ...sc, shots: sc.shots.filter(sh => sh.id !== shotId) };
      });
      if (captured) {
        pushUndo({ type: 'deleteShot', ...captured });
        setToast('Shot deleted · ⌘Z to undo');
        setTimeout(() => setToast(null), 2400);
      }
      return next;
    });
  };

  const duplicateShot = (shotId) => {
    if (guardFrameLimit()) return;
    setScenes(prev => prev.map(sc => {
      const i = sc.shots.findIndex(sh => sh.id === shotId);
      if (i === -1) return sc;
      const orig = sc.shots[i];
      const copy = { ...orig, id: 's' + Math.random().toString(36).slice(2,8) };
      const next = [...sc.shots];
      next.splice(i + 1, 0, copy);
      return { ...sc, shots: next };
    }));
  };

  const reorderInScene = (sceneId, fromId, toId, side) => {
    setScenes(prev => prev.map(sc => {
      if (sc.id !== sceneId) return sc;
      const fromIdx = sc.shots.findIndex(s => s.id === fromId);
      const toIdx = sc.shots.findIndex(s => s.id === toId);
      if (fromIdx === -1 || toIdx === -1) return sc;
      const arr = [...sc.shots];
      const [moved] = arr.splice(fromIdx, 1);
      let insertAt = arr.findIndex(s => s.id === toId);
      if (side === 'after') insertAt += 1;
      arr.splice(insertAt, 0, moved);
      return { ...sc, shots: arr };
    }));
  };

  const reorderAcrossScenes = (fromSceneId, fromId, toSceneId, toId, side) => {
    setScenes(prev => {
      const next = prev.map(sc => {
        if (sc.id !== fromSceneId) return sc;
        return { ...sc, shots: sc.shots.filter(s => s.id !== fromId) };
      });
      const moved = prev.find(sc => sc.id === fromSceneId)?.shots.find(s => s.id === fromId);
      if (!moved) return prev;
      return next.map(sc => {
        if (sc.id !== toSceneId) return sc;
        let insertAt = sc.shots.findIndex(s => s.id === toId);
        if (insertAt === -1) return { ...sc, shots: [...sc.shots, moved] };
        if (side === 'after') insertAt += 1;
        const arr = [...sc.shots];
        arr.splice(insertAt, 0, moved);
        return { ...sc, shots: arr };
      });
    });
  };

  const toggleScene = (id) => setCollapsed(c => ({ ...c, [id]: !c[id] }));

  // Apply a reordered scene/shot structure (from the Reorder modal) and
  // re-number the scenes. Shot order within each scene is taken as-is.
  const applyReorder = (ordered) => {
    setScenes(ordered.map((s, i) => ({ ...s, number: String(i + 1).padStart(2, '0') })));
  };

  let cursor = 0;
  const startNums = {};
  scenes.forEach(sc => { startNums[sc.id] = cursor + 1; cursor += sc.shots.length; });

  const safeAddShot = () => {
    if (guardFrameLimit()) return;
    if (scenes.length === 0) {
      addScene();
      // Defer the addShot since the new scene hasn't been committed yet.
      setTimeout(() => {
        setScenes(prev => {
          if (!prev.length) return prev;
          const target = prev[0];
          const newShot = {
            id: 's' + Math.random().toString(36).slice(2,8),
            title: '', desc: '', shot: '', camera: '', lens: '', move: '', duration: '', location: '',
            status: 'draft', img: '', custom: {}
          };
          return [{ ...target, shots: [...target.shots, newShot] }, ...prev.slice(1)];
        });
      }, 0);
    } else {
      addShot(scenes[0].id);
    }
  };

  // Header breadcrumbs reflect the current project — "Projects" links home.
  const crumbs = [
    { label: 'Projects', href: '/shot-list/' },
    project.title || 'Untitled',
  ];

  return (
    <div className="app" data-screen-label="Shot List">
      <Rail page="shots" />
      <main className="main">
        <Header theme={theme} setTheme={setTheme} crumbs={crumbs} />
        {(saveError || conflict || (signedIn === false && meta && !offlineDismissed)) && (
          <div className="durability-banners">
            {saveError && (
              <div className="durability-banner warn">
                <span>{saveError.quota
                  ? "Your last change couldn't be saved — this browser's storage is full. Sign in to back up to the cloud, or remove some large images."
                  : "Your last change couldn't be saved locally. Sign in to back up to the cloud so you don't lose work."}</span>
              </div>
            )}
            {conflict && (
              <div className="durability-banner conflict">
                <span>This shot list was changed on another device since you opened it.</span>
                <span className="banner-actions">
                  <button className="btn" onClick={() => location.reload()}>Reload latest</button>
                  <button className="banner-x" onClick={() => setConflict(false)} aria-label="Dismiss">×</button>
                </span>
              </div>
            )}
            {signedIn === false && meta && !offlineDismissed && (
              <div className="durability-banner info">
                <span>You're working offline — sign in to back up this shot list to the cloud.</span>
                <span className="banner-actions">
                  <button className="btn" onClick={() => { if (window.__jumpOpenAuth) window.__jumpOpenAuth(); }}>Sign in</button>
                  <button className="banner-x" onClick={dismissOffline} aria-label="Dismiss">×</button>
                </span>
              </div>
            )}
          </div>
        )}
        <ProjectSubheader
          shotCount={allShots.length}
          approvedCount={approved}
          project={project}
          onShare={() => setShowShareModal(true)}
        />

        <div className="toolbar">
          <div className="seg" role="tablist" aria-label="View">
            <button className={view==='cards' ? 'active' : ''} onClick={() => setView('cards')}>
              <IconGrid /> Cards
            </button>
            <button className={view==='table' ? 'active' : ''} onClick={() => setView('table')}>
              <IconRows /> Table
            </button>
          </div>

          <div className="seg" role="group" aria-label="Grouping">
            <button className={grouping==='scenes' ? 'active' : ''} onClick={() => setGrouping('scenes')} title="Show scene breakdown">
              <IconLayers /> Scenes + Shots
            </button>
            <button className={grouping==='shots' ? 'active' : ''} onClick={() => setGrouping('shots')} title="Flat shot list">
              <IconShotsOnly /> Shots only
            </button>
          </div>

          {view === 'cards' && (
            <div className="seg" role="group" aria-label="Card density">
              <button className={density==='full' ? 'active' : ''} onClick={() => setDensity('full')} title="Full cards">
                <IconCardFull /> Full
              </button>
              <button className={density==='compact' ? 'active' : ''} onClick={() => setDensity('compact')} title="Compact cards">
                <IconCardCompact /> Compact
              </button>
            </div>
          )}

          <div className="ar-picker" role="group" aria-label="Aspect ratio">
            {[
              { id: '16-9', label: '16:9' },
              { id: '4-3',  label: '4:3'  },
              { id: '1-1',  label: '1:1'  },
            ].map(o => (
              <button key={o.id} className={ar===o.id ? 'active' : ''} onClick={() => setAr(o.id)}>
                <span className={`ar-shape ar-${o.id}`} />
                {o.label}
              </button>
            ))}
          </div>

          <div className="toolbar-spacer" />

          <button className="btn" onClick={() => setShowReorderModal(true)}><IconLayers /> Reorder</button>
          <button className="btn" onClick={() => setShowProjectModal(true)}><IconEdit /> Project details</button>
          <button className="btn" onClick={() => setShowDataModal(true)}><IconColumns /> Data input</button>
          <button className="btn" onClick={() => setShowHistory(true)}><IconClock /> History</button>
          <button className="btn" onClick={() => window.print()}><IconExport /> Export PDF</button>
        </div>

        <div className="scroll">
          {view === 'cards' ? (
            <>
              {grouping === 'scenes' ? scenes.map(sc => (
                <SceneGroup
                  key={sc.id}
                  scene={sc}
                  ar={ar}
                  density={density}
                  dataConfig={dataConfig}
                  collapsed={!!collapsed[sc.id]}
                  onToggle={() => toggleScene(sc.id)}
                  onUpdate={updateShot}
                  onUpdateScene={updateScene}
                  onDeleteScene={deleteScene}
                  onChangeStatus={changeStatus}
                  onAddShot={addShot}
                  onDelete={deleteShot}
                  onDuplicate={duplicateShot}
                  onReorder={reorderInScene}
                  startNum={startNums[sc.id]}
                  projectId={meta && meta.id}
                  onViewComments={openComments}
                  commentCounts={commentCounts}
                />
              )) : (
                <FlatShotsGrid
                  scenes={scenes}
                  ar={ar}
                  density={density}
                  dataConfig={dataConfig}
                  onUpdate={updateShot}
                  onChangeStatus={changeStatus}
                  onDelete={deleteShot}
                  onDuplicate={duplicateShot}
                  onAddShot={safeAddShot}
                  onReorder={reorderAcrossScenes}
                  projectId={meta && meta.id}
                  onViewComments={openComments}
                  commentCounts={commentCounts}
                />
              )}
              {grouping === 'scenes' && (
                <button className="add-scene-bar" onClick={addScene}>
                  <IconPlus /> Add a scene
                </button>
              )}
            </>
          ) : (
            <ShotTable
              scenes={scenes}
              ar={ar}
              dataConfig={dataConfig}
              onChangeStatus={changeStatus}
              onUpdate={updateShot}
              onReorder={reorderAcrossScenes}
              onDelete={deleteShot}
              onDuplicate={duplicateShot}
              flat={grouping === 'shots'}
              projectId={meta && meta.id}
            />
          )}
        </div>
      </main>

      {/* Free-tier PDF watermark — hidden on screen, shown when printing. */}
      {!ent.solo && <div className="pdf-watermark" aria-hidden="true">Made with Jump Studio · jumpstud.io</div>}

      {toast && <div className="toast">{toast}</div>}

      {showProjectModal && (
        <ProjectDetailsModal
          project={project}
          onSave={setProject}
          onClose={() => setShowProjectModal(false)}
        />
      )}
      {showDataModal && (
        <DataInputModal
          config={dataConfig}
          onSave={setDataConfig}
          onClose={() => setShowDataModal(false)}
        />
      )}
      {showReorderModal && (
        <ReorderModal
          scenes={scenes}
          onSave={applyReorder}
          onClose={() => setShowReorderModal(false)}
        />
      )}
      {showShareModal && (
        <ShareLinkModal
          projectId={meta && meta.id}
          onClose={() => setShowShareModal(false)}
        />
      )}
      {galleryIndex !== null && CommentGallery && (
        <CommentGallery
          shotListId={meta && meta.id}
          scenes={scenes}
          dataConfig={dataConfig}
          startIndex={galleryIndex}
          onClose={() => setGalleryIndex(null)}
          onChanged={refreshCommentCounts}
        />
      )}
      {showHistory && (
        <VersionHistoryModal
          projectId={meta && meta.id}
          onRestore={restoreVersion}
          onClose={() => setShowHistory(false)}
        />
      )}
    </div>
  );
}

// ──────────────── Share link modal ────────────────
// Mints (or reuses) a read-only review link for this project and shows it with
// a one-tap Copy. Link creation is owner-only on the server (review_create_link
// checks ownership), so we require a signed-in session and make sure the list
// is pushed to the cloud first (the RPC reads the shot_lists row by id).
function ShareLinkModal({ projectId, onClose }) {
  const [status, setStatus] = useStateApp('loading'); // loading | signin | ready | error
  const [url, setUrl]       = useStateApp('');
  const [errMsg, setErrMsg] = useStateApp('');
  const [copied, setCopied] = useStateApp(false);

  useEffectApp(() => {
    let alive = true;
    (async () => {
      const sb = window.supabaseClient;
      if (!sb)        { if (alive) { setErrMsg('Cloud sync isn’t configured, so sharing is unavailable.'); setStatus('error'); } return; }
      if (!projectId) { if (alive) { setErrMsg('Save this project before creating a share link.'); setStatus('error'); } return; }
      const { data: { session } } = await sb.auth.getSession();
      if (!session) { if (alive) setStatus('signin'); return; }
      try {
        // Ensure the list exists in the cloud (and is current) before minting a link.
        if (typeof window.ppPushOne === 'function') { try { await window.ppPushOne(projectId); } catch (e) {} }
        const { data, error } = await sb.rpc('review_create_link', { p_shot_list_id: projectId });
        if (error) throw error;
        if (!alive) return;
        setUrl(`${window.location.origin}/shot-list/?review=${data}`);
        setStatus('ready');
      } catch (e) {
        if (alive) { setErrMsg(e.message || String(e)); setStatus('error'); }
      }
    })();
    return () => { alive = false; };
  }, [projectId]);

  const copy = async () => {
    try {
      await navigator.clipboard.writeText(url);
      setCopied(true);
      setTimeout(() => setCopied(false), 1600);
    } catch (e) { /* user can still select + copy manually */ }
  };

  return ReactDOM.createPortal(
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 460 }}>
        <h2>Share for review</h2>

        {status === 'loading' && <p className="modal-sub">Creating your link…</p>}

        {status === 'signin' && (
          <>
            <p className="modal-sub">Sign in to create a shareable review link for this project.</p>
            <div className="modal-actions">
              <button className="btn" onClick={onClose}>Cancel</button>
              <button
                className="btn btn-primary"
                onClick={() => { onClose(); if (window.__jumpOpenAuth) window.__jumpOpenAuth(); }}
              >
                Sign in
              </button>
            </div>
          </>
        )}

        {status === 'error' && (
          <>
            <p className="modal-sub">{errMsg}</p>
            <div className="modal-actions"><button className="btn" onClick={onClose}>Close</button></div>
          </>
        )}

        {status === 'ready' && (
          <>
            <p className="modal-sub">
              Anyone with this link can <strong style={{ color: 'var(--text)' }}>view</strong> the
              shot list. Reviewers sign in to leave comments.
            </p>
            <div className="share-link-row">
              <input
                className="share-link-input"
                readOnly
                value={url}
                onFocus={(e) => e.target.select()}
              />
              <button className="btn btn-primary share-copy-btn" onClick={copy}>
                {copied ? 'Copied!' : 'Copy'}
              </button>
            </div>
            <p className="share-link-hint">Send the same link to as many reviewers as you like.</p>
            <div className="modal-actions"><button className="btn" onClick={onClose}>Done</button></div>
          </>
        )}
      </div>
    </div>,
    document.body
  );
}

// ──────────────── Version history modal ────────────────
// Lists the cloud snapshots (owner-only) and restores one into the editor.
function VersionHistoryModal({ projectId, onRestore, onClose }) {
  const [versions, setVersions] = useStateApp(null); // null = loading

  useEffectApp(() => {
    let alive = true;
    (async () => {
      const list = (typeof window.ppListVersions === 'function' && projectId)
        ? await window.ppListVersions(projectId)
        : [];
      if (alive) setVersions(list);
    })();
    return () => { alive = false; };
  }, [projectId]);

  return ReactDOM.createPortal(
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 460 }}>
        <h2>Version history</h2>
        <p className="modal-sub">
          Automatic cloud snapshots of this shot list. Restoring one rolls the editor
          back — your current version is snapshotted first, so it's reversible.
        </p>

        {versions === null && <p className="modal-sub">Loading…</p>}
        {versions !== null && versions.length === 0 && (
          <p className="modal-sub">No snapshots yet — they start accruing as you edit a signed-in (cloud-synced) project.</p>
        )}
        {versions !== null && versions.length > 0 && (
          <div className="version-list">
            {versions.map((v) => (
              <div key={v.id} className="version-row">
                <span className="version-when">{new Date(v.created_at).toLocaleString()}</span>
                <button
                  className="btn btn-ghost"
                  onClick={() => { if (confirm('Restore this version? Your current version will be saved as a snapshot first.')) onRestore(v.id); }}
                >
                  Restore
                </button>
              </div>
            ))}
          </div>
        )}

        <div className="modal-actions"><button className="btn" onClick={onClose}>Close</button></div>
      </div>
    </div>,
    document.body
  );
}

// ──────────────── Reorder modal — scenes + shots ────────────────
// Drag a scene row to reorder scenes. Drag a shot row to move it within its
// scene, before/after another shot, or onto a scene header to move it into
// that scene. Scene numbers re-assign on save.
function ReorderModal({ scenes, onSave, onClose }) {
  // Working copy — clone scenes and their shots arrays.
  const [items, setItems] = useStateApp(() => scenes.map(sc => ({ ...sc, shots: [...sc.shots] })));
  // drag = { kind:'scene'|'shot', id, overKind:'scene'|'scene-into'|'shot', overId, side }
  const [drag, setDrag] = useStateApp(null);

  // ---- pure reorder ops ----
  const moveScene = (arr, fromId, toId, side) => {
    const a = [...arr];
    const fi = a.findIndex(s => s.id === fromId);
    if (fi === -1) return arr;
    const [m] = a.splice(fi, 1);
    let ti = a.findIndex(s => s.id === toId);
    if (ti === -1) return arr;
    if (side === 'after') ti += 1;
    a.splice(ti, 0, m);
    return a;
  };
  const moveShot = (arr, fromShotId, toShotId, side) => {
    let moved = null;
    const a = arr.map(sc => {
      const i = sc.shots.findIndex(s => s.id === fromShotId);
      if (i === -1) return sc;
      moved = sc.shots[i];
      return { ...sc, shots: sc.shots.filter(s => s.id !== fromShotId) };
    });
    if (!moved) return arr;
    return a.map(sc => {
      let i = sc.shots.findIndex(s => s.id === toShotId);
      if (i === -1) return sc;
      if (side === 'after') i += 1;
      const shots = [...sc.shots];
      shots.splice(i, 0, moved);
      return { ...sc, shots };
    });
  };
  const moveShotToScene = (arr, fromShotId, toSceneId) => {
    let moved = null;
    const a = arr.map(sc => {
      const i = sc.shots.findIndex(s => s.id === fromShotId);
      if (i === -1) return sc;
      moved = sc.shots[i];
      return { ...sc, shots: sc.shots.filter(s => s.id !== fromShotId) };
    });
    if (!moved) return arr;
    return a.map(sc => sc.id === toSceneId ? { ...sc, shots: [...sc.shots, moved] } : sc);
  };

  // ---- scene-row drag handlers ----
  const sceneHandlers = (sceneId) => ({
    onDragStart: (e) => {
      e.dataTransfer.effectAllowed = 'move';
      setDrag({ kind: 'scene', id: sceneId, overKind: null, overId: null, side: null });
    },
    onDragOver: (e) => {
      if (!drag) return;
      e.preventDefault();
      if (drag.kind === 'scene') {
        if (drag.id === sceneId) return;
        const r = e.currentTarget.getBoundingClientRect();
        const side = (e.clientY - r.top) < r.height / 2 ? 'before' : 'after';
        setDrag(d => d && (d.overId !== sceneId || d.side !== side || d.overKind !== 'scene')
          ? { ...d, overKind: 'scene', overId: sceneId, side } : d);
      } else {
        // a shot dragged onto a scene header → move into that scene
        setDrag(d => d && (d.overId !== sceneId || d.overKind !== 'scene-into')
          ? { ...d, overKind: 'scene-into', overId: sceneId, side: null } : d);
      }
    },
    onDrop: (e) => {
      e.preventDefault();
      if (!drag) return;
      if (drag.kind === 'scene' && drag.id !== sceneId) {
        setItems(arr => moveScene(arr, drag.id, sceneId, drag.side || 'before'));
      } else if (drag.kind === 'shot') {
        setItems(arr => moveShotToScene(arr, drag.id, sceneId));
      }
      setDrag(null);
    },
    onDragEnd: () => setDrag(null),
  });

  // ---- shot-row drag handlers ----
  const shotHandlers = (shotId) => ({
    onDragStart: (e) => {
      e.dataTransfer.effectAllowed = 'move';
      setDrag({ kind: 'shot', id: shotId, overKind: null, overId: null, side: null });
    },
    onDragOver: (e) => {
      if (!drag || drag.kind !== 'shot' || drag.id === shotId) return;
      e.preventDefault();
      const r = e.currentTarget.getBoundingClientRect();
      const side = (e.clientY - r.top) < r.height / 2 ? 'before' : 'after';
      setDrag(d => d && (d.overId !== shotId || d.side !== side || d.overKind !== 'shot')
        ? { ...d, overKind: 'shot', overId: shotId, side } : d);
    },
    onDrop: (e) => {
      if (!drag || drag.kind !== 'shot') return;
      e.preventDefault();
      if (drag.id !== shotId) setItems(arr => moveShot(arr, drag.id, shotId, drag.side || 'before'));
      setDrag(null);
    },
    onDragEnd: () => setDrag(null),
  });

  let runNum = 0;

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>
        <h2>Reorder</h2>
        <p className="modal-sub">Drag a scene to reorder scenes. Drag a shot to move it within a scene, or onto another scene's header to move it across. Scene numbers update on save.</p>

        <div className="di-list">
          {items.length === 0 && (
            <div style={{ color: 'var(--text-3)', fontSize: 13, padding: '12px 2px' }}>
              No scenes yet — add one from the board first.
            </div>
          )}
          {items.map((scene, si) => {
            const sh = sceneHandlers(scene.id);
            const over = drag && drag.overId === scene.id;
            const sceneCls = [
              'di-row', 'reorder-scene-row',
              drag?.kind === 'scene' && drag.id === scene.id ? 'dragging' : '',
              over && drag.overKind === 'scene' && drag.side === 'before' ? 'drop-before' : '',
              over && drag.overKind === 'scene' && drag.side === 'after' ? 'drop-after' : '',
              over && drag.overKind === 'scene-into' ? 'reorder-into' : '',
            ].filter(Boolean).join(' ');
            return (
              <React.Fragment key={scene.id}>
                <div
                  className={sceneCls}
                  draggable
                  onDragStart={sh.onDragStart}
                  onDragOver={sh.onDragOver}
                  onDrop={sh.onDrop}
                  onDragEnd={sh.onDragEnd}
                  style={{ gridTemplateColumns: '22px auto 1fr auto' }}
                >
                  <div className="di-handle" title="Drag scene"><IconGrip /></div>
                  <span className="scene-num">SC {String(si + 1).padStart(2, '0')}</span>
                  <div className="di-row-label">{scene.title || 'Untitled scene'}</div>
                  <span style={{ color: 'var(--text-3)', fontSize: 12, fontFamily: 'var(--font-mono)' }}>
                    {scene.shots.length} {scene.shots.length === 1 ? 'shot' : 'shots'}
                  </span>
                </div>
                {scene.shots.map(shot => {
                  runNum += 1;
                  const shh = shotHandlers(shot.id);
                  const sOver = drag && drag.kind === 'shot' && drag.overKind === 'shot' && drag.overId === shot.id;
                  const shotCls = [
                    'di-row', 'reorder-shot-row',
                    drag?.kind === 'shot' && drag.id === shot.id ? 'dragging' : '',
                    sOver && drag.side === 'before' ? 'drop-before' : '',
                    sOver && drag.side === 'after' ? 'drop-after' : '',
                  ].filter(Boolean).join(' ');
                  return (
                    <div
                      key={shot.id}
                      className={shotCls}
                      draggable
                      onDragStart={shh.onDragStart}
                      onDragOver={shh.onDragOver}
                      onDrop={shh.onDrop}
                      onDragEnd={shh.onDragEnd}
                      style={{ gridTemplateColumns: '22px 30px 1fr', marginLeft: 26 }}
                    >
                      <div className="di-handle" title="Drag shot"><IconGrip /></div>
                      <span className="reorder-shot-num">{runNum}</span>
                      <div className="di-row-label" style={{ fontWeight: 400, color: 'var(--text-2)' }}>
                        {shot.title || 'Untitled shot'}
                      </div>
                    </div>
                  );
                })}
              </React.Fragment>
            );
          })}
        </div>

        <div className="modal-actions">
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn btn-primary" onClick={() => { onSave(items); onClose(); }}>
            <IconCheck /> Save order
          </button>
        </div>
      </div>
    </div>
  );
}

// Toolbar icons defined inline in the design's app.jsx — keep them here.
const IconLayers = (props) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...props}>
    <path d="M12 3l9 5-9 5-9-5 9-5z"/>
    <path d="M3 13l9 5 9-5"/>
  </svg>
);
const IconShotsOnly = (props) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...props}>
    <rect x="4" y="5" width="6.5" height="14" rx="1.5"/>
    <rect x="13.5" y="5" width="6.5" height="14" rx="1.5"/>
  </svg>
);
const IconCardCompact = (props) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" {...props}>
    <rect x="3.5" y="4.5" width="17" height="7" rx="1.5"/>
    <rect x="3.5" y="13.5" width="17" height="6" rx="1.5"/>
  </svg>
);
const IconCardFull = (props) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" {...props}>
    <rect x="3.5" y="3.5" width="17" height="17" rx="1.5"/>
    <path d="M3.5 9h17M3.5 13h17M3.5 17h17"/>
  </svg>
);

window.ShotListApp = ShotListApp;
