// Client Review Dashboard — read-only reviewer view.
//
// Reached at /shot-list/?review=<token>. Renders a read-only mirror of the
// shot-list editor (scene groups + shot cards, no edit affordances), and lets
// the reviewer click any shot to open a full-screen slideshow with the image,
// shot details and a comment thread that both client and owner use. The owner
// can tick (resolve) comments.
//
// Phase 2 (live): the shot list is fetched from Supabase via the token-scoped
// review_get_state RPC (granted to anon → viewing needs no login). Comments are
// persisted via review_list_comments / review_add_comment / review_resolve_comment;
// leaving a comment requires a real signed-in account (gated behind "Sign in to
// comment"). When window.supabaseClient is absent (e.g. a bare static preview),
// it falls back to the mock demo data so the design still renders.
//
// Wrapped in an IIFE so its locals never collide with the other shot-list
// scripts, which share one global lexical scope (top-level `const` in one
// <script> would clash with the same name in another). Only window.ReviewApp
// leaks out.
(function () {
  const { useState, useEffect, useRef, useMemo } = React;

  // ──────────────────────────────────────────── local SVG icons
  // (the global icon set has no X / chevron-left / comment glyphs)
  const IconX = (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" {...p}>
      <path d="M6 6l12 12M18 6L6 18" />
    </svg>
  );
  const IconPrev = (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M15 5l-7 7 7 7" />
    </svg>
  );
  const IconNext = (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M9 5l7 7-7 7" />
    </svg>
  );
  const IconComment = (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M21 11.5a8.38 8.38 0 0 1-8.5 8.5 9 9 0 0 1-4-1L3 20l1-5.5a8.38 8.38 0 0 1-1-4A8.5 8.5 0 0 1 11.5 2 8.5 8.5 0 0 1 21 11.5z" />
    </svg>
  );
  const IconEye = (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7z" />
      <circle cx="12" cy="12" r="3" />
    </svg>
  );
  // Grouping-toggle glyphs — copied from the editor (shot-list-app.jsx) so the
  // reviewer toolbar matches it exactly without depending on its top-level consts.
  const IconLayers = (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M12 3l9 5-9 5-9-5 9-5z" />
      <path d="M3 13l9 5 9-5" />
    </svg>
  );
  const IconShotsOnly = (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <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 IconReply = (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M9 14L4 9l5-5" />
      <path d="M4 9h10a6 6 0 0 1 6 6v3" />
    </svg>
  );
  const IconLock = (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <rect x="4" y="11" width="16" height="10" rx="2" />
      <path d="M8 11V7a4 4 0 0 1 8 0v4" />
    </svg>
  );

  // ──────────────────────────────────────────── mock data
  const MOCK_PROJECT = {
    title: 'Run the City',
    kind: 'Commercial — Aurora Active',
    dates: 'May 18–20',
    crew: { director: 'Maya Okonkwo', dop: 'Liam Reyes', production: 'North Field', agency: 'Bold & Sons' },
    formats: ['ARRI Mini LF', 'Anamorphic'],
  };
  // Mirrors DEFAULT_DATA_CONFIG in shot-list-app.jsx.
  const MOCK_DATA_CONFIG = {
    enabled: { shot: true, camera: false, lens: true, move: true, duration: true, location: false },
    custom: [],
    order: [],
  };

  // A few seeded comments keyed by shot id so the thread has something to show.
  const HOUR = 1000 * 60 * 60;
  const SEED_COMMENTS = {
    s2: [
      { id: 'c1', author: 'Dana (Client)', body: 'Love the warmth here — can we linger a touch longer on the trainers? It is the hero product moment.', createdAt: Date.now() - HOUR * 5, resolved: false },
    ],
    s6: [
      { id: 'c2', author: 'Dana (Client)', body: 'Could she enter from the right instead? Reads better with the bridge behind her.', createdAt: Date.now() - HOUR * 26, resolved: false },
      { id: 'c3', author: 'Maya (Director)', body: 'Good call — we will flip the blocking on the day.', createdAt: Date.now() - HOUR * 25, resolved: true },
    ],
    s11: [
      { id: 'c4', author: 'Dana (Client)', body: 'This is the money shot. Make sure the endcard logo timing lands cleanly on the pull-up.', createdAt: Date.now() - HOUR * 0.5, resolved: false },
    ],
  };

  // Offline / preview fallback — the rich demo scenes, used when Supabase isn't
  // configured or there's no token (so the design still renders on a bare server).
  function buildMockState() {
    return { project: MOCK_PROJECT, dataConfig: MOCK_DATA_CONFIG, scenes: window.SCENES || [], isOwner: false };
  }

  // Shape the review_get_state payload ({ title, state, is_owner }) — where
  // `state` is the editor's saved snapshot — into the reviewer's render model.
  function shapeCloudState(payload) {
    const st = (payload && payload.state) || {};
    const project = st.project || {
      title: (payload && payload.title) || 'Shot List', kind: '', dates: '', crew: {}, formats: [],
    };
    return {
      project,
      dataConfig: st.dataConfig || MOCK_DATA_CONFIG,
      scenes: Array.isArray(st.scenes) ? st.scenes : [],
      isOwner: !!(payload && payload.is_owner),
    };
  }

  // ──────────────────────────────────────────── helpers
  function timeAgo(ts) {
    const s = Math.max(0, Math.floor((Date.now() - ts) / 1000));
    if (s < 60) return 'just now';
    const m = Math.floor(s / 60);
    if (m < 60) return m + 'm ago';
    const h = Math.floor(m / 60);
    if (h < 24) return h + 'h ago';
    const d = Math.floor(h / 24);
    if (d < 7) return d + 'd ago';
    return new Date(ts).toLocaleDateString();
  }

  // Flatten all shots across scenes into one ordered list for slideshow nav.
  function flattenShots(scenes) {
    const flat = [];
    let n = 0;
    (scenes || []).forEach((scene) => {
      (scene.shots || []).forEach((shot) => {
        n += 1;
        flat.push({ shot, scene, displayNum: n });
      });
    });
    return flat;
  }

  const FIELD_ICON_NAMES = {
    shot: 'IconShotType', camera: 'IconCameraBody', lens: 'IconAperture',
    move: 'IconArrows', duration: 'IconClock', location: 'IconLocation',
  };

  // ──────────────────────────────────────────── shared sub-components
  function ReviewStatusPill({ status }) {
    const s = (window.STATUSES && window.STATUSES[status]) || { label: status, cls: 'draft' };
    return (
      <span className={`status-pill ${s.cls}`}>
        <span className="pulse" />
        {s.label}
      </span>
    );
  }

  // Static field rows mirroring the editor's full-density card grid.
  function ReviewFieldRows({ shot, dataConfig }) {
    const builtins = window.BUILTIN_FIELDS || [];
    const order = (dataConfig.order && dataConfig.order.length)
      ? dataConfig.order
      : [
          ...builtins.map((f) => ({ key: f.key, kind: 'builtin' })),
          ...(dataConfig.custom || []).map((c) => ({ key: c.key, kind: 'custom' })),
        ];
    return order.map((o) => {
      if (o.kind === 'builtin') {
        const f = builtins.find((x) => x.key === o.key);
        if (!f || !dataConfig.enabled[f.key]) return null;
        const Icon = window[FIELD_ICON_NAMES[f.key]];
        const val = shot[f.key];
        return (
          <div key={f.key} className="shot-form-row">
            {Icon ? <Icon className="row-icon" /> : <span className="row-icon" />}
            <span className="row-label-full">{f.label}</span>
            <span className="row-value">{val || '—'}</span>
          </div>
        );
      }
      const cf = (dataConfig.custom || []).find((c) => c.key === o.key);
      if (!cf) return null;
      const val = shot.custom && shot.custom[cf.key];
      return (
        <div key={cf.key} className="shot-form-row">
          <span className="row-icon di-dot-icon"><span className="di-dot di-dot-grey" /></span>
          <span className="row-label-full">{cf.label}</span>
          <span className="row-value">{val || '—'}</span>
        </div>
      );
    });
  }

  // ──────────────────────────────────────────── reviewer subheader
  // A slimmed copy of chrome.jsx's ProjectSubheader: same project banner +
  // approval progress chip, but no owner-only "Projects Manager" button. Kept
  // local so the editor's shared ProjectSubheader stays untouched.
  function ReviewSubheader({ shotCount, approvedCount, project }) {
    const pct = shotCount === 0 ? 0 : Math.round((approvedCount / shotCount) * 100);
    const crew = project.crew || {};
    const labels = { director: 'Director', dop: 'DoP', production: 'Production', agency: 'Agency' };
    const items = [];
    ['director', 'dop', 'production', 'agency'].forEach((k) => {
      if (crew[k]) items.push(<span key={k}><strong>{labels[k]}</strong> {crew[k]}</span>);
    });
    if (project.dates) items.push(<span key="dates"><strong>Shoot</strong> {project.dates}</span>);
    const fmts = (project.formats || []).filter(Boolean);
    if (fmts.length) {
      items.push(<span key="formats"><strong>{fmts.length === 1 ? 'Format' : 'Formats'}:</strong> {fmts.join(', ')}</span>);
    }
    const meta = items.flatMap((node, i) => i === 0 ? [node] : [<span key={'s' + i} className="sep">·</span>, node]);
    return (
      <div className="subheader">
        <div className="subheader-left">
          <div className="subheader-eyebrow"><span className="dot" /> {project.kind || 'Commercial'}</div>
          <h1>{project.title}</h1>
          <div className="subheader-meta">{meta}</div>
        </div>
        <div className="subheader-right">
          <div className="progress-chip" title={`${approvedCount} of ${shotCount} approved`}>
            <div className="progress-track">
              <div className="progress-fill" style={{ width: `${pct}%` }} />
            </div>
            <span><strong style={{ color: 'var(--text)' }}>{approvedCount}</strong>/{shotCount} approved</span>
          </div>
        </div>
      </div>
    );
  }

  // ──────────────────────────────────────────── read-only grid
  function ReviewShotCard({ shot, displayNum, ar, dataConfig, onOpen, commentCount, unresolved }) {
    return (
      <div className="shot-card density-full review-card" onClick={onOpen}>
        <div className={`shot-thumb ar-${ar}`}>
          {shot.img ? (
            <div className="thumb-img" style={{ backgroundImage: `url(${shot.img})` }} />
          ) : (
            <div className="review-empty"><IconImageEmpty /><span>No image</span></div>
          )}
          <div className="shot-num-badge">{displayNum}</div>
          {commentCount > 0 && (
            <div className={`review-comment-badge ${unresolved > 0 ? 'has-open' : ''}`} title={`${commentCount} comment${commentCount === 1 ? '' : 's'}`}>
              <IconComment />{commentCount}
            </div>
          )}
        </div>
        <div className="shot-body">
          <div className="review-title">{shot.title || 'Untitled shot'}</div>
          {shot.desc ? <div className="review-desc">{shot.desc}</div> : null}
          <div className="shot-form-grid full">
            <ReviewFieldRows shot={shot} dataConfig={dataConfig} />
          </div>
          <div className="shot-status-row">
            <ReviewStatusPill status={shot.status} />
            <span className="review-open-hint">View comments <IconNext /></span>
          </div>
        </div>
      </div>
    );
  }

  function ReviewSceneGroup({ scene, ar, dataConfig, startNum, collapsed, onToggle, onOpenShot, commentCounts }) {
    const shots = scene.shots || [];
    return (
      <section className={`scene ${collapsed ? 'collapsed' : ''}`}>
        <div className="scene-header" onClick={onToggle}>
          <div className="scene-chev"><IconChevDown /></div>
          <span className="scene-num">SC {scene.number}</span>
          <div className="scene-title-wrap">
            <span className="review-scene-title">{scene.title || 'Untitled scene'}</span>
            <span className="scene-count">{shots.length} {shots.length === 1 ? 'shot' : 'shots'}</span>
          </div>
          <div className="scene-line" />
        </div>
        <div className="scene-body">
          <div className="cards-grid">
            {shots.map((s, i) => {
              const cc = commentCounts[s.id];
              return (
                <ReviewShotCard
                  key={s.id}
                  shot={s}
                  displayNum={startNum + i}
                  ar={ar}
                  dataConfig={dataConfig}
                  onOpen={() => onOpenShot(startNum - 1 + i)}
                  commentCount={cc ? cc.total : 0}
                  unresolved={cc ? cc.unresolved : 0}
                />
              );
            })}
          </div>
        </div>
      </section>
    );
  }

  // ──────────────────────────────────────────── comments
  // Phase 2: identity comes from a real signed-in account. Viewing is open, but
  // the composer is gated behind "Sign in to comment". Reply targets a parent
  // comment (threaded server-side + drives reply notifications) and @-mentions
  // its author in the draft. Only the list owner sees the resolve control.
  function CommentThread({ shotId, list, onAdd, onToggleResolve, canResolve, isSignedIn, onSignIn, notifyOnReply, onSetNotify, showNotify = true }) {
    const [draft, setDraft] = useState('');
    const [replyTo, setReplyTo] = useState(null); // { id, author }
    const taRef = useRef(null);

    const submit = () => {
      const body = draft.trim();
      if (!body) return;
      onAdd(shotId, body, replyTo ? replyTo.id : null);
      setDraft('');
      setReplyTo(null);
    };

    // Reply = target a parent comment + @-mention its author in the draft. Strips
    // a trailing role in parens, e.g. "Dana (Client)" → "@Dana ".
    const startReply = (c) => {
      setReplyTo({ id: c.id, author: c.author });
      const name = String(c.author || '').replace(/\s*\(.*\)\s*$/, '').trim();
      const mention = name ? '@' + name + ' ' : '';
      setDraft((d) => (mention && !d.startsWith(mention) ? mention + d : d));
      requestAnimationFrame(() => {
        const ta = taRef.current;
        if (ta) { ta.focus(); const len = ta.value.length; try { ta.setSelectionRange(len, len); } catch (e) {} }
      });
    };

    return (
      <div className="comment-thread">
        <div className="comment-thread-head">
          <IconComment /> Comments <span className="comment-count">{list.length}</span>
        </div>

        <div className="comment-list">
          {list.length === 0 && (
            <div className="comment-empty">No comments yet — start the conversation.</div>
          )}
          {list.map((c) => (
            <div key={c.id} className={`comment ${c.resolved ? 'resolved' : ''}`}>
              <div className="comment-top">
                <span className="comment-author">{c.author}</span>
                <span className="comment-time">{timeAgo(c.createdAt)}</span>
                {canResolve ? (
                  <button
                    className={`comment-resolve ${c.resolved ? 'on' : ''}`}
                    onClick={() => onToggleResolve(shotId, c.id, !c.resolved)}
                    title={c.resolved ? 'Mark unresolved' : 'Mark resolved'}
                  >
                    <IconCheck /> {c.resolved ? 'Resolved' : 'Resolve'}
                  </button>
                ) : (c.resolved ? (
                  <span className="comment-resolved-tag"><IconCheck /> Resolved</span>
                ) : null)}
              </div>
              <div className="comment-body">{c.body}</div>
              {isSignedIn && (
                <div className="comment-actions">
                  <button className="comment-reply" onClick={() => startReply(c)}>
                    <IconReply /> Reply
                  </button>
                </div>
              )}
            </div>
          ))}
        </div>

        <div className="comment-add">
          {isSignedIn ? (
            <>
              {replyTo && (
                <div className="comment-replying">
                  Replying to <strong>{replyTo.author}</strong>
                  <button className="linklike" onClick={() => setReplyTo(null)}>cancel</button>
                </div>
              )}
              <div className="comment-add-row">
                <textarea
                  ref={taRef}
                  className="comment-input"
                  rows={2}
                  placeholder="Add a comment…  (⌘/Ctrl + Enter to send)"
                  value={draft}
                  onChange={(e) => setDraft(e.target.value)}
                  onKeyDown={(e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) submit(); }}
                />
                <button className="btn btn-primary comment-send" onClick={submit}>Send</button>
              </div>
              {showNotify && (
                <label className="comment-notify">
                  <input type="checkbox" checked={!!notifyOnReply} onChange={(e) => onSetNotify(e.target.checked)} />
                  Email me when someone replies
                </label>
              )}
            </>
          ) : (
            <button className="comment-signin" onClick={onSignIn}>
              <IconLock /> Sign in to comment
            </button>
          )}
        </div>
      </div>
    );
  }

  // ──────────────────────────────────────────── slideshow overlay
  function ReviewSlideshow({ flat, index, onPrev, onNext, onClose, dataConfig, commentsForShot, onAdd, onToggleResolve, canResolve, isSignedIn, onSignIn, notifyOnReply, onSetNotify, showNotify }) {
    useEffect(() => {
      const onKey = (e) => {
        if (e.key === 'Escape') onClose();
        else if (e.key === 'ArrowLeft') onPrev();
        else if (e.key === 'ArrowRight') onNext();
      };
      window.addEventListener('keydown', onKey);
      return () => window.removeEventListener('keydown', onKey);
    }, [onPrev, onNext, onClose]);

    const item = flat[index];
    if (!item) return null;
    const { shot, scene, displayNum } = item;

    return ReactDOM.createPortal(
      <div className="slideshow-backdrop" onClick={onClose}>
        <div className="slideshow" onClick={(e) => e.stopPropagation()}>
          <button className="slideshow-close" onClick={onClose} aria-label="Close"><IconX /></button>

          <div className="slideshow-stage">
            {index > 0 && (
              <button className="slideshow-nav prev" onClick={onPrev} aria-label="Previous shot"><IconPrev /></button>
            )}
            {shot.img ? (
              <img className="slideshow-img" src={shot.img} alt={shot.title || 'Shot'} />
            ) : (
              <div className="slideshow-img empty"><IconImageEmpty /><span>No image</span></div>
            )}
            {index < flat.length - 1 && (
              <button className="slideshow-nav next" onClick={onNext} aria-label="Next shot"><IconNext /></button>
            )}
            <div className="slideshow-counter">{index + 1} / {flat.length}</div>
          </div>

          <aside className="slideshow-side">
            <div className="slideshow-head">
              <div className="slideshow-eyebrow">SC {scene.number} · Shot {displayNum}</div>
              <h2 className="slideshow-title">{shot.title || 'Untitled shot'}</h2>
              <ReviewStatusPill status={shot.status} />
            </div>
            {shot.desc ? <p className="slideshow-desc">{shot.desc}</p> : null}
            <div className="slideshow-details shot-form-grid full">
              <ReviewFieldRows shot={shot} dataConfig={dataConfig} />
            </div>
            <CommentThread
              shotId={shot.id}
              list={commentsForShot}
              onAdd={onAdd}
              onToggleResolve={onToggleResolve}
              canResolve={canResolve}
              isSignedIn={isSignedIn}
              onSignIn={onSignIn}
              notifyOnReply={notifyOnReply}
              onSetNotify={onSetNotify}
              showNotify={showNotify}
            />
          </aside>
        </div>
      </div>,
      document.body
    );
  }

  // ──────────────────────────────────────────── top-level app
  function ReviewApp({ token }) {
    const sb = window.supabaseClient || null;

    const [theme, setTheme] = useState(() => {
      try { return localStorage.getItem('pp-theme') || 'dark'; } catch (e) { return 'dark'; }
    });
    useEffect(() => {
      document.documentElement.setAttribute('data-theme', theme);
      try { localStorage.setItem('pp-theme', theme); } catch (e) {}
    }, [theme]);

    // ── auth: a real signed-in account is required to comment ──
    const [user, setUser] = useState(null);
    const [showAuth, setShowAuth] = useState(false);
    useEffect(() => {
      if (!sb) return;
      let mounted = true;
      sb.auth.getSession().then(({ data }) => { if (mounted) setUser(data?.session?.user ?? null); });
      const { data: sub } = sb.auth.onAuthStateChange((_e, session) => { if (mounted) setUser(session?.user ?? null); });
      return () => { mounted = false; sub?.subscription?.unsubscribe?.(); };
    }, []);

    // ── load the shot-list body via the token RPC (mock fallback offline) ──
    // status: 'loading' | 'ready' | 'notfound' | 'error'
    const [data, setData] = useState({ status: 'loading', project: null, dataConfig: null, scenes: [], isOwner: false, error: '' });
    useEffect(() => {
      let alive = true;
      (async () => {
        if (!sb || !token) {                 // design/preview fallback
          if (alive) setData({ status: 'ready', ...buildMockState(), error: '' });
          return;
        }
        try {
          const { data: payload, error } = await sb.rpc('review_get_state', { p_token: token });
          if (error) throw error;
          if (!payload) { if (alive) setData((d) => ({ ...d, status: 'notfound' })); return; }
          if (alive) setData({ status: 'ready', ...shapeCloudState(payload), error: '' });
        } catch (e) {
          if (alive) setData((d) => ({ ...d, status: 'error', error: e.message || String(e) }));
        }
      })();
      return () => { alive = false; };
    }, [token]);

    const project    = data.project || { title: '', kind: '', crew: {}, formats: [] };
    const dataConfig = data.dataConfig || MOCK_DATA_CONFIG;
    const scenes     = data.scenes || [];
    const isOwner    = !!data.isOwner;

    // ── comments: loaded from the cloud, refetched after every mutation ──
    const [comments, setComments] = useState({});
    const [notifyOnReply, setNotifyOnReply] = useState(true);
    async function loadComments() {
      if (!sb || !token) { setComments(JSON.parse(JSON.stringify(SEED_COMMENTS))); return; }
      try {
        const { data: rows, error } = await sb.rpc('review_list_comments', { p_token: token });
        if (error) throw error;
        const grouped = {};
        (rows || []).forEach((c) => {
          (grouped[c.shot_id] = grouped[c.shot_id] || []).push({
            id: c.id, author: c.author_name, body: c.body,
            createdAt: new Date(c.created_at).getTime(), resolved: c.resolved,
            parentId: c.parent_id, authorId: c.author_id,
          });
        });
        setComments(grouped);
      } catch (e) { /* keep what we have */ }
    }
    useEffect(() => { loadComments(); }, [token]);

    const addComment = async (shotId, body, parentId) => {
      if (!sb || !token) {                   // mock append
        setComments((prev) => {
          const next = { ...prev };
          const arr = next[shotId] ? next[shotId].slice() : [];
          const who = (user && (user.user_metadata?.full_name || user.email)) || 'You';
          arr.push({ id: 'c' + Math.random().toString(36).slice(2, 9), author: who, body, createdAt: Date.now(), resolved: false, parentId: parentId || null });
          next[shotId] = arr;
          return next;
        });
        return;
      }
      const { error } = await sb.rpc('review_add_comment', { p_token: token, p_shot_id: shotId, p_body: body, p_parent_id: parentId || null });
      if (error) { alert(error.message || 'Could not post your comment.'); return; }
      await loadComments();
    };

    const toggleResolve = async (shotId, commentId, resolved) => {
      if (!sb || !token) {                   // mock toggle
        setComments((prev) => {
          const next = { ...prev };
          next[shotId] = (next[shotId] || []).map((c) => c.id === commentId ? { ...c, resolved } : c);
          return next;
        });
        return;
      }
      const { error } = await sb.rpc('review_resolve_comment', { p_comment_id: commentId, p_resolved: resolved });
      if (error) { alert(error.message || 'Could not update that comment.'); return; }
      await loadComments();
    };

    const setNotify = async (val) => {
      setNotifyOnReply(val);
      if (sb && token && user) { try { await sb.rpc('review_set_notify', { p_token: token, p_notify: val }); } catch (e) {} }
    };

    const [ar, setAr] = useState('16-9');
    const [grouping, setGrouping] = useState('scenes'); // 'scenes' | 'shots'
    const [collapsed, setCollapsed] = useState({});
    const [openIndex, setOpenIndex] = useState(null);

    const commentCounts = useMemo(() => {
      const m = {};
      Object.keys(comments).forEach((sid) => {
        const arr = comments[sid] || [];
        m[sid] = { total: arr.length, unresolved: arr.filter((c) => !c.resolved).length };
      });
      return m;
    }, [comments]);

    const flat = useMemo(() => flattenShots(scenes), [scenes]);
    const allShots = flat.map((f) => f.shot);
    const approved = allShots.filter((s) => s.status === 'approved').length;
    const totalComments = Object.keys(commentCounts).reduce((n, k) => n + commentCounts[k].total, 0);
    const openComments = Object.keys(commentCounts).reduce((n, k) => n + commentCounts[k].unresolved, 0);

    // Per-scene starting shot number (continues across scenes).
    let cursor = 0;
    const startNums = {};
    scenes.forEach((sc) => { startNums[sc.id] = cursor + 1; cursor += (sc.shots || []).length; });

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

    const openSlideshowShot = (gi) => setOpenIndex(gi);
    const prev = () => setOpenIndex((i) => Math.max(0, i - 1));
    const next = () => setOpenIndex((i) => Math.min(flat.length - 1, i + 1));
    const close = () => setOpenIndex(null);

    const Header = window.Header;
    const AuthModal = window.AuthModal;
    const isSignedIn = !!user;
    const onSignIn = () => {
      // The "Sign in to comment" button lives inside the slideshow (z-index
      // 1000); the auth modal is z-index 100, so opened underneath it would be
      // invisible. Close the slideshow first. (A real sign-in redirects/reloads
      // the page anyway, so the slideshow wasn't going to survive regardless.)
      setOpenIndex(null);
      if (AuthModal) setShowAuth(true);
      else if (window.__jumpOpenAuth) window.__jumpOpenAuth();
    };

    // ── loading / not-found / error gates (all hooks above already ran) ──
    if (data.status !== 'ready') {
      return (
        <div className="app review-app" data-screen-label="Client Review">
          <main className="main">
            {Header ? <Header theme={theme} setTheme={setTheme} crumbs={['Client Review']} /> : null}
            <div className="review-state">
              {data.status === 'loading' && (
                <div className="review-state-inner"><div className="review-spinner" /><p>Loading review…</p></div>
              )}
              {data.status === 'notfound' && (
                <div className="review-state-inner"><h2>Link not found</h2><p>This review link is invalid, expired, or has been revoked. Ask the owner for a fresh one.</p></div>
              )}
              {data.status === 'error' && (
                <div className="review-state-inner"><h2>Couldn’t load this review</h2><p>{data.error}</p></div>
              )}
            </div>
          </main>
          {showAuth && AuthModal && <AuthModal onClose={() => setShowAuth(false)} />}
        </div>
      );
    }

    return (
      <div className="app review-app" data-screen-label="Client Review">
        <main className="main">
          {Header ? <Header theme={theme} setTheme={setTheme} crumbs={['Client Review']} /> : null}
          <ReviewSubheader shotCount={allShots.length} approvedCount={approved} project={project} />

          <div className="toolbar review-toolbar">
            <div className="review-badge"><IconEye /> Read-only review</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>
            <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" />
            <div className="review-comment-summary">
              <IconComment /> {totalComments} comment{totalComments === 1 ? '' : 's'}
              {openComments > 0 ? <span className="review-open-count"> · {openComments} open</span> : null}
            </div>
          </div>

          <div className="scroll">
            {grouping === 'scenes' ? (
              scenes.map((sc) => (
                <ReviewSceneGroup
                  key={sc.id}
                  scene={sc}
                  ar={ar}
                  dataConfig={dataConfig}
                  startNum={startNums[sc.id]}
                  collapsed={!!collapsed[sc.id]}
                  onToggle={() => toggleScene(sc.id)}
                  onOpenShot={openSlideshowShot}
                  commentCounts={commentCounts}
                />
              ))
            ) : (
              <section className="scene flat-shots">
                <div className="scene-body">
                  <div className="cards-grid">
                    {flat.map((f, i) => {
                      const cc = commentCounts[f.shot.id];
                      return (
                        <ReviewShotCard
                          key={f.shot.id}
                          shot={f.shot}
                          displayNum={f.displayNum}
                          ar={ar}
                          dataConfig={dataConfig}
                          onOpen={() => openSlideshowShot(i)}
                          commentCount={cc ? cc.total : 0}
                          unresolved={cc ? cc.unresolved : 0}
                        />
                      );
                    })}
                  </div>
                </div>
              </section>
            )}
          </div>
        </main>

        {openIndex !== null && (
          <ReviewSlideshow
            flat={flat}
            index={openIndex}
            onPrev={prev}
            onNext={next}
            onClose={close}
            dataConfig={dataConfig}
            commentsForShot={(comments[flat[openIndex] && flat[openIndex].shot.id]) || []}
            onAdd={addComment}
            onToggleResolve={toggleResolve}
            canResolve={isOwner}
            isSignedIn={isSignedIn}
            onSignIn={onSignIn}
            notifyOnReply={notifyOnReply}
            onSetNotify={setNotify}
          />
        )}

        {showAuth && AuthModal && <AuthModal onClose={() => setShowAuth(false)} />}
      </div>
    );
  }

  // ──────────────────────────────────────────── owner comment gallery
  // Reusable from the EDITOR (window.CommentGallery). Same slideshow + thread
  // as the reviewer, but the owner reads/writes via ownership-gated RPCs (no
  // share token needed), can always resolve, and the notify checkbox is hidden.
  function CommentGallery({ shotListId, scenes, dataConfig, startIndex, onClose, onChanged }) {
    const sb = window.supabaseClient || null;
    const flat = useMemo(() => flattenShots(scenes), [scenes]);
    const [index, setIndex] = useState(startIndex || 0);
    const [comments, setComments] = useState({});

    async function loadComments() {
      if (!sb || !shotListId) return;
      try {
        const { data: rows, error } = await sb.rpc('review_owner_list_comments', { p_shot_list_id: shotListId });
        if (error) throw error;
        const grouped = {};
        (rows || []).forEach((c) => {
          (grouped[c.shot_id] = grouped[c.shot_id] || []).push({
            id: c.id, author: c.author_name, body: c.body,
            createdAt: new Date(c.created_at).getTime(), resolved: c.resolved,
            parentId: c.parent_id, authorId: c.author_id,
          });
        });
        setComments(grouped);
      } catch (e) { /* keep what we have */ }
    }
    useEffect(() => { loadComments(); }, [shotListId]);

    const addComment = async (shotId, body, parentId) => {
      if (!sb) return;
      const { error } = await sb.rpc('review_owner_add_comment', {
        p_shot_list_id: shotListId, p_shot_id: shotId, p_body: body, p_parent_id: parentId || null,
      });
      if (error) { alert(error.message || 'Could not post your comment.'); return; }
      await loadComments();
      if (onChanged) onChanged();
    };
    const toggleResolve = async (shotId, commentId, resolved) => {
      if (!sb) return;
      const { error } = await sb.rpc('review_resolve_comment', { p_comment_id: commentId, p_resolved: resolved });
      if (error) { alert(error.message || 'Could not update that comment.'); return; }
      await loadComments();
      if (onChanged) onChanged();
    };

    const prev = () => setIndex((i) => Math.max(0, i - 1));
    const next = () => setIndex((i) => Math.min(flat.length - 1, i + 1));

    if (!flat.length) return null;
    const commentsForShot = (comments[flat[index] && flat[index].shot.id]) || [];

    return (
      <ReviewSlideshow
        flat={flat}
        index={index}
        onPrev={prev}
        onNext={next}
        onClose={onClose}
        dataConfig={dataConfig}
        commentsForShot={commentsForShot}
        onAdd={addComment}
        onToggleResolve={toggleResolve}
        canResolve={true}
        isSignedIn={true}
        onSignIn={() => {}}
        notifyOnReply={false}
        onSetNotify={() => {}}
        showNotify={false}
      />
    );
  }

  window.CommentGallery = CommentGallery;
  window.ReviewApp = ReviewApp;
})();
