// Scenes.jsx — multi-scene management for Block Builder
//
// A "scene" snapshots the editable state of the builder:
//   { id, name, kind, blocks, accent, externals, canvas, github? }
// Scenes live in localStorage under "bb.scenes.v1" and are auto-saved
// whenever any of the underlying state changes.
//
// Exports (attached to window):
//   useScenes()               → React hook returning { scenes, activeId,
//                               active, setActiveId, createManual,
//                               createFromGithub, renameScene, deleteScene,
//                               patchActive }
//   <SceneTabs ... />         → top-bar pill tabs + "+" with popdown menu
//   <SceneTransitions ... />  → small "scene transitions" panel shown
//                               above Canvas/Layers/Export when >1 scene
//
// The hook is the single source of truth: App.jsx mirrors `active.*`
// into its own state on scene-switch, and pipes state changes back
// through patchActive(...) so persistence is automatic.

const { useState: _sUseState, useEffect: _sUseEffect, useRef: _sUseRef, useMemo: _sUseMemo, useCallback: _sUseCallback } = React;

// localStorage keys are namespaced by auth mode + userId so guest sessions
// can never see a previous OAuth user's cached scenes (and two different
// OAuth users on the same browser stay isolated from each other too).
//   guest:  bb.scenes.v1::guest
//   oauth:  bb.scenes.v1::user.<userId>
//   bypass: bb.scenes.v1::user.local-dev
function _ns() {
  const mode = window.BB_AUTH_MODE;
  if (mode === 'guest') return 'guest';
  const userId = window.BB_USER?.user || (mode === 'bypass' ? 'local-dev' : 'anonymous');
  return `user.${userId}`;
}
function _key(base) { return `bb.${base}.v1::${_ns()}`; }
const SCENES_KEY      = () => _key('scenes');
const ACTIVE_KEY      = () => _key('scenes.active');
const TRANSITIONS_KEY = () => _key('scenes.transitions');

// One-time migration from the legacy un-namespaced keys (v=11 and earlier)
// AND from the buggy v=13 user.anonymous namespace (which was created when the
// migration accidentally ran before AuthGate set BB_AUTH_MODE). We only
// migrate INTO an authenticated namespace — never into 'guest', so a previous
// user's local cache doesn't leak into a guest session.
function _migrateLegacyKeysOnce() {
  try {
    if (window.BB_AUTH_MODE === 'guest' || !window.BB_AUTH_MODE) return;
    if (sessionStorage.getItem('bb.legacy.migrated.v2') === _ns()) return;
    const sourceNamespaces = ['', '::user.anonymous']; // legacy + buggy intermediate
    for (const src of sourceNamespaces) {
      for (const base of ['scenes', 'scenes.active', 'scenes.transitions']) {
        const oldK = src ? `bb.${base}.v1${src}` : `bb.${base}.v1`;
        const newK = _key(base);
        if (oldK === newK) continue;
        const v = localStorage.getItem(oldK);
        if (v != null && localStorage.getItem(newK) == null) {
          localStorage.setItem(newK, v);
        }
        localStorage.removeItem(oldK);
      }
    }
    sessionStorage.setItem('bb.legacy.migrated.v2', _ns());
  } catch {}
}

// ----- default scene factory -----
function _defaultScene(name = 'Scene 1') {
  return {
    id: 's_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 7),
    name,
    kind: 'manual',
    canvas: { W: 2, D: 2, H: 2 },
    blocks: [
      { id: 'seed-1', color: 'purple',  type: 'solid', label: 'Cache', gx: 0, gy: 0, gz: 0, dx: 1, dy: 1, dz: 1 },
      { id: 'seed-2', color: 'magenta', type: 'solid', label: 'API',   gx: 0, gy: 0, gz: 1, dx: 1, dy: 1, dz: 1 },
      { id: 'seed-3', color: 'blue',    type: 'solid', label: 'NoSQL', gx: 1, gy: 0, gz: 0, dx: 1, dy: 1, dz: 1 },
    ],
    accent: null,
    externals: [],
    github: null,
  };
}

// ---- Data sanitizer ----
// Older builds (and the now-removed mock auto-agent) sometimes wrote scenes
// with colors that aren't in the current PALETTE — 'teal', 'amber', or other
// stray values. The renderer already falls back to a safe color, but we also
// coerce on load so bad values don't quietly survive into Harper / localStorage.
const _PRIMARY_COLORS = new Set(['magenta', 'purple', 'blue', 'green']);
const _EXTERNAL_COLORS = new Set(['slate', 'slateNeutral', 'slateCool']);
function _sanitizeScene(s) {
  if (!s || typeof s !== 'object') return s;
  const blocks = Array.isArray(s.blocks)
    ? s.blocks.map((b) => (_PRIMARY_COLORS.has(b?.color) ? b : { ...b, color: 'green' }))
    : s.blocks;
  let accent = s.accent;
  if (accent && !_PRIMARY_COLORS.has(accent.color)) accent = { ...accent, color: 'green' };
  const externals = Array.isArray(s.externals)
    ? s.externals.map((e) => (_EXTERNAL_COLORS.has(e?.color) ? e : { ...e, color: 'slate' }))
    : s.externals;
  return { ...s, blocks, accent, externals };
}

function _emptyScene(name) {
  return {
    id: 's_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 7),
    name,
    kind: 'manual',
    canvas: { W: 2, D: 2, H: 2 },
    blocks: [],
    accent: null,
    externals: [],
    github: null,
  };
}

function _loadScenes() {
  try {
    const raw = localStorage.getItem(SCENES_KEY());
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    if (!Array.isArray(parsed) || parsed.length === 0) return null;
    // Sanitize on the way in: stray colors from old builds get coerced to
    // a safe value so they can't crash the renderer.
    return parsed.map(_sanitizeScene);
  } catch (e) { return null; }
}
function _loadActive() {
  try { return localStorage.getItem(ACTIVE_KEY()) || null; } catch { return null; }
}
function _saveScenes(scenes) {
  try { localStorage.setItem(SCENES_KEY(), JSON.stringify(scenes)); } catch {}
}
function _saveActive(id) {
  try { localStorage.setItem(ACTIVE_KEY(), id || ''); } catch {}
}

// ---- The hook ----
function useScenes() {
  // Run the legacy-key migration before initial state load so the lazy
  // useState initializers below see the migrated values for this user.
  _migrateLegacyKeysOnce();
  const [scenes, _setScenesRaw] = _sUseState(() => _loadScenes() || [_defaultScene()]);
  const [activeId, _setActiveId] = _sUseState(() => {
    const stored = _loadActive();
    const list = _loadScenes() || [];
    if (stored && list.find((s) => s.id === stored)) return stored;
    return (list[0] && list[0].id) || null;
  });

  // ----- Undo / redo history -----
  // We snapshot { scenes, activeId } before each user-initiated mutation
  // and coalesce snapshots that arrive within 300ms (so a drag or a fast
  // sequence of nudges becomes a single undo step). The stack is capped at
  // 50 entries so memory stays bounded even on long sessions.
  const HISTORY_LIMIT = 50;
  const COALESCE_MS = 300;
  const _historyRef = _sUseRef({
    past: [],
    future: [],
    coalesceTimer: null,
    applying: false, // true while undo/redo is being applied -> skip snapshotting
  });
  const _activeIdRef = _sUseRef(activeId);
  _sUseEffect(() => { _activeIdRef.current = activeId; }, [activeId]);
  const [_historyEpoch, _setHistoryEpoch] = _sUseState(0);

  // History-aware setScenes. Pass { skipHistory: true } to bypass snapshots
  // (used for non-user-initiated updates like loading from Harper).
  const setScenes = _sUseCallback((updater, opts) => {
    _setScenesRaw((prev) => {
      const h = _historyRef.current;
      if (!h.applying && !opts?.skipHistory) {
        if (h.coalesceTimer) {
          clearTimeout(h.coalesceTimer);
        } else {
          h.past.push({ scenes: prev, activeId: _activeIdRef.current });
          if (h.past.length > HISTORY_LIMIT) h.past.shift();
          h.future = [];
        }
        h.coalesceTimer = setTimeout(() => { h.coalesceTimer = null; }, COALESCE_MS);
      }
      return typeof updater === 'function' ? updater(prev) : updater;
    });
  }, []);

  const undo = _sUseCallback(() => {
    const h = _historyRef.current;
    if (!h.past.length) return;
    if (h.coalesceTimer) { clearTimeout(h.coalesceTimer); h.coalesceTimer = null; }
    const snapshot = h.past.pop();
    _setScenesRaw((curr) => {
      h.future.push({ scenes: curr, activeId: _activeIdRef.current });
      if (h.future.length > HISTORY_LIMIT) h.future.shift();
      return snapshot.scenes;
    });
    _setActiveId(snapshot.activeId);
    h.applying = true;
    setTimeout(() => { h.applying = false; }, 0);
    _setHistoryEpoch((n) => n + 1);
  }, []);

  const redo = _sUseCallback(() => {
    const h = _historyRef.current;
    if (!h.future.length) return;
    if (h.coalesceTimer) { clearTimeout(h.coalesceTimer); h.coalesceTimer = null; }
    const snapshot = h.future.pop();
    _setScenesRaw((curr) => {
      h.past.push({ scenes: curr, activeId: _activeIdRef.current });
      if (h.past.length > HISTORY_LIMIT) h.past.shift();
      return snapshot.scenes;
    });
    _setActiveId(snapshot.activeId);
    h.applying = true;
    setTimeout(() => { h.applying = false; }, 0);
    _setHistoryEpoch((n) => n + 1);
  }, []);

  // Persist scenes & activeId on change — localStorage (instant) + Harper (durable)
  const _harperSaveTimer = _sUseRef(null);
  _sUseEffect(() => { _saveScenes(scenes); }, [scenes]);
  _sUseEffect(() => { _saveActive(activeId); }, [activeId]);

  // Debounced sync to Harper via WebSocket — only when an authenticated session
  // (or local AUTH_BYPASS) has flipped on the BB_HARPER_ENABLED flag.
  // In guest mode we stay localStorage-only, so this effect short-circuits.
  _sUseEffect(() => {
    if (!window.HarperSync || !window.BB_HARPER_ENABLED) return;
    clearTimeout(_harperSaveTimer.current);
    _harperSaveTimer.current = setTimeout(() => {
      scenes.forEach((s, i) => window.HarperSync.saveScene(s, i));
      window.HarperSync.saveAppState(activeId, null);
    }, 600);
  }, [scenes, activeId]);

  // On mount: try loading from Harper (overwrites localStorage if Harper has data)
  const _harperLoaded = _sUseRef(false);
  _sUseEffect(() => {
    if (_harperLoaded.current || !window.HarperSync || !window.BB_HARPER_ENABLED) return;
    _harperLoaded.current = true;
    (async () => {
      try {
        const harperScenesRaw = await window.HarperSync.loadScenes();
        const harperScenes = Array.isArray(harperScenesRaw) ? harperScenesRaw.map(_sanitizeScene) : harperScenesRaw;
        if (harperScenes && harperScenes.length > 0) {
          // Initial Harper load isn't a user action — don't push it onto
          // the undo stack (otherwise the user's first Cmd+Z would wipe
          // the just-loaded scenes back to the default seed scene).
          setScenes(harperScenes, { skipHistory: true });
          const appState = await window.HarperSync.loadAppState();
          if (appState?.activeSceneId) {
            const exists = harperScenes.find(s => s.id === appState.activeSceneId);
            if (exists) _setActiveId(appState.activeSceneId);
          }
        } else {
          // First run: seed Harper with localStorage data
          const local = _loadScenes();
          if (local && local.length > 0) {
            local.forEach((s, i) => window.HarperSync.saveScene(s, i));
          }
        }
        // Open WebSocket for ongoing sync
        window.HarperSync.connect();
      } catch (e) {
        console.warn('[Scenes] Harper load failed, using localStorage', e);
      }
    })();
  }, []);

  // If activeId points at a missing scene, fall back to first.
  _sUseEffect(() => {
    if (!scenes.length) return;
    if (!scenes.find((s) => s.id === activeId)) {
      _setActiveId(scenes[0].id);
    }
  }, [scenes, activeId]);

  const active = _sUseMemo(
    () => scenes.find((s) => s.id === activeId) || scenes[0] || null,
    [scenes, activeId]
  );

  const setActiveId = _sUseCallback((id) => _setActiveId(id), []);

  const patchActive = _sUseCallback((patch) => {
    setScenes((ss) =>
      ss.map((s) => (s.id === activeId ? { ...s, ...patch } : s))
    );
  }, [activeId]);

  const renameScene = _sUseCallback((id, name) => {
    setScenes((ss) => ss.map((s) => (s.id === id ? { ...s, name } : s)));
  }, []);

  const deleteScene = _sUseCallback((id) => {
    setScenes((ss) => {
      if (ss.length <= 1) return ss; // never delete the last
      const idx = ss.findIndex((s) => s.id === id);
      const next = ss.filter((s) => s.id !== id);
      // If we deleted the active scene, jump to neighbour
      if (id === activeId) {
        const fallback = next[Math.max(0, idx - 1)] || next[0];
        if (fallback) _setActiveId(fallback.id);
      }
      // Sync delete to Harper (only in authenticated/bypass mode; guests are localStorage-only)
      if (window.HarperSync && window.BB_HARPER_ENABLED) window.HarperSync.deleteScene(id);
      return next;
    });
  }, [activeId]);

  const createManual = _sUseCallback((name) => {
    const s = _emptyScene(name);
    setScenes((ss) => [...ss, s]);
    _setActiveId(s.id);
    return s;
  }, []);

  // Reorder by moving the scene at fromIdx to position toIdx. Both are
  // 0-indexed positions in the live `scenes` array. The hook is the source
  // of truth for animation playback order, so reordering chips here also
  // reorders Animation Preview legs automatically.
  const reorderScenes = _sUseCallback((fromIdx, toIdx) => {
    setScenes((ss) => {
      if (fromIdx === toIdx) return ss;
      if (fromIdx < 0 || fromIdx >= ss.length) return ss;
      const clamped = Math.max(0, Math.min(ss.length - 1, toIdx));
      const next = ss.slice();
      const [moved] = next.splice(fromIdx, 1);
      next.splice(clamped, 0, moved);
      return next;
    });
  }, []);

  const createFromGithub = _sUseCallback(async (name, repoUrl) => {
    const s = {
      ..._emptyScene(name),
      kind: 'auto',
      github: { url: repoUrl, status: 'analyzing', startedAt: Date.now() },
    };
    setScenes((ss) => [...ss, s]);
    _setActiveId(s.id);

    // Hit the server-side /SceneAgent resource. It does the GitHub fetch +
    // Claude call + validation + gravity-settle, and returns a payload
    // matching the same shape _runMockAgent used to produce, plus the
    // accentSuggestions array driving the chip strip in App.jsx.
    (async () => {
      try {
        const res = await fetch('/SceneAgent', {
          method: 'POST',
          credentials: 'include',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ url: repoUrl }),
        });
        const body = await res.json().catch(() => ({}));
        if (!res.ok) {
          // Harper returns RFC 9457 Problem Details on errors:
          //   { type, code, title, status, detail, instance }
          // We surface `title` (the user-facing message) plus an optional hint
          // from `detail.hint` so the scene tab tooltip explains what to do.
          const title = body?.title || body?.message || `Agent failed (HTTP ${res.status})`;
          const hint = body?.detail?.hint;
          throw new Error(hint ? `${title} ${hint}` : title);
        }
        // The server already returns canvas, blocks, accent, accentSuggestions, externals.
        const { canvas, blocks, accent, accentSuggestions, externals } = body;
        setScenes((ss) =>
          ss.map((sc) =>
            sc.id === s.id
              ? {
                  ...sc,
                  canvas, blocks, accent, accentSuggestions, externals,
                  github: { ...sc.github, status: 'ready', completedAt: Date.now() },
                }
              : sc
          )
        );
        // The new scene was already active when the placeholder mounted, so
        // _activeSceneId hasn't changed — App's mirror effect won't re-fire on
        // its own. Bump _historyEpoch to force the mirror to repopulate
        // canvas/blocks/accent/externals from the now-filled-in active scene.
        _setHistoryEpoch((n) => n + 1);
      } catch (err) {
        setScenes((ss) =>
          ss.map((sc) =>
            sc.id === s.id
              ? { ...sc, github: { ...sc.github, status: 'error', error: err.message || String(err) } }
              : sc
          )
        );
      }
    })();
    return s;
  }, []);

  return {
    scenes, activeId, active,
    setActiveId, patchActive, renameScene, deleteScene,
    createManual, createFromGithub, reorderScenes,
    undo, redo, historyEpoch: _historyEpoch,
  };
}

// (The deterministic placeholder _runMockAgent that used to live here is
// gone — `createFromGithub` now POSTs to /SceneAgent which does the real
// thing. See resources/sceneAgent.js + resources/lib/*. Git history has
// the old implementation if you ever need it back as a fallback.)

// =====================================================================
//                              UI
// =====================================================================

// ---- Scene tabs (header) ----
function SceneTabs({
  scenes, activeId, onActivate, onCreate, onRename, onDelete, onReorder,
}) {
  const [menuOpen, setMenuOpen] = _sUseState(false);
  const [editingId, setEditingId] = _sUseState(null);
  // Drag-reorder state. dragId = id of the chip being dragged; overId =
  // id of the chip currently hovered (the "drop target"). Both are null
  // when no drag is in progress.
  const [dragId, setDragId] = _sUseState(null);
  const [overId, setOverId] = _sUseState(null);
  const plusRef = _sUseRef(null);

  // Close popdown on outside click / Esc
  _sUseEffect(() => {
    if (!menuOpen) return;
    const onDown = (e) => {
      if (plusRef.current && plusRef.current.contains(e.target)) return;
      const menu = document.getElementById('scene-create-menu');
      if (menu && menu.contains(e.target)) return;
      setMenuOpen(false);
    };
    const onKey = (e) => { if (e.key === 'Escape') setMenuOpen(false); };
    document.addEventListener('mousedown', onDown);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDown);
      document.removeEventListener('keydown', onKey);
    };
  }, [menuOpen]);

  return (
    <div className="scene-tabs" role="tablist" aria-label="Scenes">
      {scenes.map((s) => {
        const active = s.id === activeId;
        const editing = editingId === s.id;
        const status = s.github?.status;
        const isDragging = dragId === s.id;
        const isOver = overId === s.id && dragId && dragId !== s.id;
        return (
          <div
            key={s.id}
            role="tab"
            aria-selected={active}
            draggable={!editing}
            className={`scene-tab ${active ? 'active' : ''} ${s.kind === 'auto' ? 'auto' : ''} ${isDragging ? 'dragging' : ''} ${isOver ? 'drop-target' : ''}`}
            onClick={() => onActivate(s.id)}
            onDoubleClick={() => setEditingId(s.id)}
            onDragStart={(e) => {
              if (editing) { e.preventDefault(); return; }
              setDragId(s.id);
              try {
                e.dataTransfer.effectAllowed = 'move';
                e.dataTransfer.setData('text/plain', s.id);
              } catch (_) {}
            }}
            onDragEnter={(e) => {
              if (!dragId || dragId === s.id) return;
              e.preventDefault();
              setOverId(s.id);
            }}
            onDragOver={(e) => {
              if (!dragId) return;
              e.preventDefault();
              if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
              if (overId !== s.id && dragId !== s.id) setOverId(s.id);
            }}
            onDragLeave={(e) => {
              if (overId === s.id) setOverId(null);
            }}
            onDrop={(e) => {
              e.preventDefault();
              if (!dragId || dragId === s.id) {
                setDragId(null); setOverId(null); return;
              }
              const fromIdx = scenes.findIndex((x) => x.id === dragId);
              const toIdx = scenes.findIndex((x) => x.id === s.id);
              if (fromIdx >= 0 && toIdx >= 0 && onReorder) onReorder(fromIdx, toIdx);
              setDragId(null);
              setOverId(null);
            }}
            onDragEnd={() => { setDragId(null); setOverId(null); }}
            title={s.kind === 'auto' ? `Auto · ${s.github?.url || ''}` : 'Click to switch · drag to reorder · double-click to rename'}
          >
            {s.kind === 'auto' && (
              <span className={`scene-tab-dot ${status || ''}`} aria-hidden="true" />
            )}
            {editing ? (
              <input
                autoFocus
                className="scene-tab-input"
                defaultValue={s.name}
                maxLength={28}
                onClick={(e) => e.stopPropagation()}
                onBlur={(e) => { onRename(s.id, e.target.value.trim() || s.name); setEditingId(null); }}
                onKeyDown={(e) => {
                  if (e.key === 'Enter') e.target.blur();
                  if (e.key === 'Escape') setEditingId(null);
                }}
              />
            ) : (
              <span className="scene-tab-name">{s.name}</span>
            )}
            {scenes.length > 1 && active && !editing && (
              <button
                type="button"
                className="scene-tab-close"
                aria-label="Delete scene"
                title="Delete scene"
                onClick={(e) => {
                  e.stopPropagation();
                  if (confirm(`Delete scene "${s.name}"?`)) onDelete(s.id);
                }}
              >×</button>
            )}
          </div>
        );
      })}
      <button
        ref={plusRef}
        type="button"
        className={`scene-tab-plus ${menuOpen ? 'open' : ''}`}
        onClick={(e) => { e.stopPropagation(); setMenuOpen((v) => !v); }}
        aria-label="Add scene"
        aria-expanded={menuOpen}
      >
        <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round">
          <line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
        </svg>
      </button>
      {menuOpen && (
        <SceneCreateMenu
          anchorRef={plusRef}
          onClose={() => setMenuOpen(false)}
          onManual={() => {
            const next = `Scene ${scenes.length + 1}`;
            onCreate({ kind: 'manual', name: next });
            setMenuOpen(false);
          }}
          onAutomated={({ url, name }) => {
            onCreate({ kind: 'auto', name, url });
            setMenuOpen(false);
          }}
        />
      )}
    </div>
  );
}

function SceneCreateMenu({ anchorRef, onClose, onManual, onAutomated }) {
  const [pos, setPos] = _sUseState(null);
  const [tab, setTab] = _sUseState('choose'); // 'choose' | 'github'
  const [url, setUrl] = _sUseState('');
  const [name, setName] = _sUseState('');
  const ref = _sUseRef(null);

  _sUseEffect(() => {
    const measure = () => {
      const a = anchorRef.current;
      if (!a) return;
      const r = a.getBoundingClientRect();
      setPos({ top: r.bottom + 8, left: r.left });
    };
    measure();
    window.addEventListener('resize', measure);
    return () => window.removeEventListener('resize', measure);
  }, [anchorRef]);

  const validUrl = /^https?:\/\/(www\.)?github\.com\/[\w.-]+\/[\w.-]+/.test(url.trim());

  return ReactDOM.createPortal(
    <div
      ref={ref}
      id="scene-create-menu"
      className="scene-create-menu"
      style={{ position: 'fixed', top: pos ? pos.top : -9999, left: pos ? pos.left : -9999, visibility: pos ? 'visible' : 'hidden' }}
      onMouseDown={(e) => e.stopPropagation()}
    >
      {tab === 'choose' && (
        <div className="scene-create-grid">
          <button type="button" className="scene-create-card" onClick={onManual}>
            <span className="scene-create-icon" aria-hidden="true">
              <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
                <path d="M12 3 L20 7.5 L20 16.5 L12 21 L4 16.5 L4 7.5 Z"/>
                <path d="M12 3 L12 12 M4 7.5 L12 12 L20 7.5"/>
              </svg>
            </span>
            <span className="scene-create-title">Manual scene</span>
            <span className="scene-create-sub">Start with an empty canvas.</span>
          </button>
          <button type="button" className="scene-create-card" onClick={() => setTab('github')}>
            <span className="scene-create-icon" aria-hidden="true">
              <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
                <path d="M9 19c-4.3 1.4-4.3-2.5-6-3M15 21v-3.5c0-1 .1-1.4-.5-2 2.8-.3 5.5-1.4 5.5-6 0-1.2-.5-2.4-1.3-3.2.4-1.2.4-2.5-.2-3.7 0 0-1.1-.3-3.5 1.3a12 12 0 0 0-6 0C6.6 1.3 5.5 1.6 5.5 1.6a4 4 0 0 0-.2 3.7A4.6 4.6 0 0 0 4 8.5c0 4.6 2.7 5.7 5.5 6-.6.6-.6 1.2-.5 2V21"/>
              </svg>
            </span>
            <span className="scene-create-title">Automated</span>
            <span className="scene-create-sub">Agent reads a Harper repo and assembles blocks.</span>
          </button>
        </div>
      )}
      {tab === 'github' && (
        <div className="scene-create-form">
          <div className="scene-create-form-title">From a Harper repo</div>
          <input
            autoFocus
            className="scene-create-input"
            placeholder="https://github.com/owner/repo"
            value={url}
            onChange={(e) => setUrl(e.target.value)}
          />
          <input
            className="scene-create-input"
            placeholder="Scene name (optional)"
            value={name}
            onChange={(e) => setName(e.target.value)}
            maxLength={28}
          />
          <div className="scene-create-form-hint">
            The agent inspects the repo, picks blocks for each Harper component, and arranges them to fill the canvas with readable labels.
          </div>
          <div className="scene-create-form-actions">
            <button type="button" className="scene-create-btn-ghost" onClick={() => setTab('choose')}>Back</button>
            <button
              type="button"
              className="scene-create-btn"
              disabled={!validUrl}
              onClick={() => {
                const auto = name.trim() || (url.match(/github\.com\/[^/]+\/([^/]+)/i)?.[1] || 'Auto scene').replace(/\.git$/, '');
                onAutomated({ url: url.trim(), name: auto });
              }}
            >Generate</button>
          </div>
        </div>
      )}
    </div>,
    document.body
  );
}

// ---- Scene transitions panel ----
//
// Sits above the right-rail when there's >1 scene. Initially minimal —
// a single header row showing "Scene Transitions" + the from→to picker.
// Expanded view exposes per-transition timing/easing + Webflow export.
function SceneTransitions({ scenes, activeId, transitions, setTransitions, onPreview, onOpenChange }) {
  const [open, setOpen] = _sUseState(false);
  _sUseEffect(() => { if (onOpenChange) onOpenChange(open); }, [open]);
  const otherScenes = scenes.filter((s) => s.id !== activeId);
  const fromScene = scenes.find((s) => s.id === activeId);

  // Publish the panel's actual rendered height to a CSS var on <html>
  // so the right-rail can shrink by exactly that amount + gap, keeping
  // the visual gap constant whether the panel is collapsed or open.
  const panelRef = _sUseRef(null);
  const _pushPanelHeight = _sUseCallback(() => {
    const el = panelRef.current;
    if (!el) return;
    const h = el.offsetHeight;
    document.documentElement.style.setProperty('--bb-anim-panel-h', h + 'px');
    document.body.style.setProperty('--bb-anim-panel-h', h + 'px');
    const rail = document.querySelector('.right-rail.with-transitions');
    if (rail) rail.style.bottom = `calc(20px + ${h}px + 12px)`;
  }, []);
  _sUseEffect(() => {
    const el = panelRef.current;
    if (!el) return;
    _pushPanelHeight();
    const ro = new ResizeObserver(() => _pushPanelHeight());
    ro.observe(el);
    return () => {
      ro.disconnect();
      document.documentElement.style.removeProperty('--bb-anim-panel-h');
      document.body.style.removeProperty('--bb-anim-panel-h');
      const rail = document.querySelector('.right-rail.with-transitions');
      if (rail) rail.style.bottom = '';
    };
  }, [_pushPanelHeight]);
  // Also push on every render so a state change (e.g. open toggle, new
  // transition row) immediately reconciles the rail height.
  _sUseEffect(() => {
    // Wait for layout — content additions take a frame.
    const id = requestAnimationFrame(() => _pushPanelHeight());
    return () => cancelAnimationFrame(id);
  });

  const ensureFor = (toId) => {
    const key = `${activeId}->${toId}`;
    return transitions[key] || { duration: 600, easing: 'ease-in-out', kind: 'morph' };
  };
  const setFor = (toId, patch) => {
    const key = `${activeId}->${toId}`;
    setTransitions({ ...transitions, [key]: { ...ensureFor(toId), ...patch } });
  };

  return (
    <div ref={panelRef} className={`scene-transitions ${open ? 'open' : ''}`}>
      <button
        type="button"
        className="scene-transitions-header"
        onClick={() => setOpen((v) => !v)}
        aria-expanded={open}
      >
        <span className="scene-transitions-title">Animation</span>
        <span className="scene-transitions-caret" aria-hidden="true">
          <svg viewBox="0 0 12 12" width="12" height="12">
            <polyline points="3,4.5 6,8 9,4.5" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        </span>
      </button>
      {open && (
        <div className="scene-transitions-body">
          {otherScenes.length === 0 ? (
            <div className="scene-transitions-empty">Add another scene to define transitions.</div>
          ) : (
            <button
              type="button"
              className="scene-transitions-export"
              onClick={() => {
                if (!onPreview) return;
                // Play each transition in sequence so the user sees every
                // from→to animation in one go. The animation choreography
                // is fixed (~1600ms per leg + dwell) and lives in App.jsx.
                const LEG = 2400 + 700; // animation + dwell between legs
                otherScenes.forEach((to, idx) => {
                  setTimeout(() => onPreview({ toId: to.id }), idx * LEG);
                });
              }}
              title="Play the transitions between scenes."
            >
              <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
                <polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none" />
              </svg>
              Preview
            </button>
          )}
        </div>
      )}
    </div>
  );
}

// Build a portable HTML snippet that Webflow accepts (Custom Code embed).
// Renders an <iframe srcdoc> wouldn't be ideal — instead we ship a
// self-contained <div> + <style> + <script> block that fades between
// pre-rendered SVG snapshots of each scene on a timer.
function buildWebflowExport({ scenes, transitions }) {
  // We rely on the live SVG that's rendered on stage right now via the
  // existing Export utility. Here we just describe the per-scene metadata
  // — actual SVG capture is delegated to the caller.
  const rows = scenes.map((s) => `<!-- Scene: ${s.name} -->`).join('\n');
  return `<!-- Block Builder · Webflow embed -->
<div class="bb-scenes">
${rows}
</div>
<style>
.bb-scenes { position: relative; aspect-ratio: 16/10; max-width: 100%; }
.bb-scenes svg { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; transition: opacity 600ms ease-in-out; }
.bb-scenes svg.active { opacity: 1; }
</style>
<script>
(function(){ var els=document.querySelectorAll('.bb-scenes svg'); var i=0; function step(){ els.forEach(function(e,k){ e.classList.toggle('active', k===i); }); i=(i+1)%els.length; setTimeout(step, 4000); } step(); })();
</script>`;
}

// ----- Hook for transitions persistence (small, separate from scenes) -----
function useSceneTransitions() {
  const [t, setT] = _sUseState(() => {
    try { return JSON.parse(localStorage.getItem(TRANSITIONS_KEY()) || '{}'); } catch { return {}; }
  });
  _sUseEffect(() => {
    try { localStorage.setItem(TRANSITIONS_KEY(), JSON.stringify(t)); } catch {}
    // Sync transitions to Harper (only in authenticated/bypass mode)
    if (window.HarperSync && window.BB_HARPER_ENABLED) window.HarperSync.saveAppState(null, t);
  }, [t]);
  return [t, setT];
}

// Expose to window so App.jsx can pull them after the script loads.
Object.assign(window, {
  useScenes,
  useSceneTransitions,
  SceneTabs,
  SceneTransitions,
  buildWebflowExport,
});
