// screens-editor.jsx - NLE timeline editor
// Depends on: window.TC (timecode.js), window.Timeline (timeline.js), window.ZAMPP_API

function _uid() { return 'ce_' + (++_uid._c || (_uid._c = 0)); }

function Editor() {
  const [projectId, setProjectId] = React.useState(null);
  const [sequences, setSequences] = React.useState([]);
  const [currentSeq, setCurrentSeq] = React.useState(null);
  const [confirm, confirmModal] = window.useConfirm();
  const [assets, setAssets] = React.useState([]);
  const [bins, setBins] = React.useState([]);
  const [sourceAsset, setSourceAsset] = React.useState(null);
  const [srcIn, setSrcIn] = React.useState(null);
  const [srcOut, setSrcOut] = React.useState(null);
  const [playing, setPlaying] = React.useState(false);
  const [playheadFrames, setPlayheadFrames] = React.useState(0);
  const [saveStatus, setSaveStatus] = React.useState('');
  const [selectedClipId, setSelectedClipId] = React.useState(null);
  const [history, setHistory] = React.useState([[]]);
  const [historyIdx, setHistoryIdx] = React.useState(0);
  const [scale, setScale] = React.useState(100);
  const [tool, setTool] = React.useState('select');
  const [seqFilter, setSeqFilter] = React.useState('');
  const [showNewSeq, setShowNewSeq] = React.useState(false);
  const [newSeqName, setNewSeqName] = React.useState('');
  const [isDirty, setIsDirty] = React.useState(false);
  const [insertTrack, setInsertTrack] = React.useState(0);
  const [renamingSeq, setRenamingSeq] = React.useState(false);
  const [renameVal, setRenameVal] = React.useState('');
  const [clipMenu, setClipMenu] = React.useState(null);
  const srcVideoRef = React.useRef(null);
  const pgmVideoRef = React.useRef(null);
  const tlRef = React.useRef(null);
  const saveTimerRef = React.useRef(null);
  const statusTimerRef = React.useRef(null);
  const streamCacheRef = React.useRef({});
  const mountedRef = React.useRef(true);
  React.useEffect(() => () => {
    mountedRef.current = false;
    if (saveTimerRef.current)   clearTimeout(saveTimerRef.current);
    if (statusTimerRef.current) clearTimeout(statusTimerRef.current);
  }, []);

  const tlInitRef = React.useRef(false);
  // Refs so Timeline callbacks always read current values without stale closure issues
  const historyRef = React.useRef([[]]);
  const historyIdxRef = React.useRef(0);
  const currentSeqRef = React.useRef(null);
  const isDirtyRef = React.useRef(false);
  const assetsRef = React.useRef([]);

  React.useEffect(() => {
    const data = window.ZAMPP_DATA;
    setAssets(data.ASSETS || []);
    setBins(data.BINS || []);
    const pid = data.PROJECTS && data.PROJECTS.length ? data.PROJECTS[0].id : null;
    setProjectId(pid);
    if (pid) loadSequences(pid);
  }, []);

  React.useEffect(() => { currentSeqRef.current = currentSeq; }, [currentSeq]);
  React.useEffect(() => { isDirtyRef.current = isDirty; }, [isDirty]);
  React.useEffect(() => { assetsRef.current = assets; }, [assets]);

  React.useEffect(() => {
    const refreshAssets = function() {
      window.ZAMPP_API.refreshAssets()
        .then(function(list) { setAssets(list); })
        .catch(function() {});
      window.ZAMPP_API.fetch('/bins')
        .then(function(list) { setBins((list || []).map(function(b) { return { ...b, count: b.asset_count || 0, icon: b.type || 'grid' }; })); })
        .catch(function() {});
    };
    const onAssetsChanged = function() { refreshAssets(); };
    window.addEventListener('df:assets-changed', onAssetsChanged);
    return function() { window.removeEventListener('df:assets-changed', onAssetsChanged); };
  }, []);

  React.useEffect(() => {
    if (!tlRef.current || tlInitRef.current || !window.Timeline) return;
    tlInitRef.current = true;
    window.Timeline.init(tlRef.current, {
      fps: 59.94,
      scale: 100,
      onClipsChanged: handleClipsChanged,
      onPlayheadMoved: handlePlayheadMoved,
      onClipContextMenu: handleClipContextMenu,
      onExternalDrop: handleExternalDrop,
    });
  }, []);

  React.useEffect(() => {
    if (window.Timeline && tlInitRef.current) {
      window.Timeline.setScale(scale);
    }
  }, [scale]);

  React.useEffect(() => {
    if (!clipMenu) return;
    const close = function() { setClipMenu(null); };
    window.addEventListener('click', close);
    window.addEventListener('contextmenu', close);
    return function() {
      window.removeEventListener('click', close);
      window.removeEventListener('contextmenu', close);
    };
  }, [clipMenu]);

  async function loadSequences(pid) {
    try {
      const r = await window.ZAMPP_API.getSequences(pid);
      const list = Array.isArray(r) ? r : [];
      setSequences(list);
      if (list.length) openSequence(list[0].id);
    } catch (e) { window.DF_LOG.warn('[editor] load sequences failed', e); }
  }

  async function openSequence(id) {
    try {
      const r = await window.ZAMPP_API.getSequence(id);
      const clips = (r.clips || []).map(c => ({ ...c, _id: _uid() }));
      const seq = { ...r, clips };
      setCurrentSeq(seq);
      currentSeqRef.current = seq;
      setPlayheadFrames(0);
      setSelectedClipId(null);
      historyRef.current = [clips];
      historyIdxRef.current = 0;
      setHistory([clips]);
      setHistoryIdx(0);
      setIsDirty(false);
      isDirtyRef.current = false;
      setSaveStatus('');
      renderTimelineClips(clips);
    } catch (e) { window.DF_LOG.warn('[editor] open sequence failed', e); }
  }

  async function createNewSequence() {
    if (!projectId) return;
    const name = newSeqName.trim() || ('Sequence ' + (sequences.length + 1));
    try {
      const r = await window.ZAMPP_API.createSequence({ project_id: projectId, name });
      setSequences(prev => [r, ...prev]);
      setNewSeqName('');
      setShowNewSeq(false);
      openSequence(r.id);
    } catch (e) { window.DF_LOG.warn('[editor] create sequence failed', e); }
  }

  async function renameSequence() {
    if (!currentSeq || !renameVal.trim()) { setRenamingSeq(false); return; }
    try {
      await window.ZAMPP_API.updateSequence(currentSeq.id, { name: renameVal.trim() });
      setSequences(prev => prev.map(s => s.id === currentSeq.id ? { ...s, name: renameVal.trim() } : s));
      setCurrentSeq(prev => prev ? { ...prev, name: renameVal.trim() } : prev);
    } catch (e) { window.DF_LOG.warn('[editor] rename failed', e); }
    setRenamingSeq(false);
  }

  async function deleteSequence() {
    if (!currentSeq) return;
    if (!(await confirm({ title: 'Delete sequence?', message: 'Delete sequence "' + currentSeq.name + '"? This cannot be undone.' }))) return;
    try {
      await window.ZAMPP_API.deleteSequence(currentSeq.id);
      const remaining = sequences.filter(s => s.id !== currentSeq.id);
      setSequences(remaining);
      setCurrentSeq(null);
      currentSeqRef.current = null;
      setHistory([[]]);
      setHistoryIdx(0);
      if (remaining.length) openSequence(remaining[0].id);
    } catch (e) { window.DF_LOG.warn('[editor] delete failed', e); }
  }

  function renderTimelineClips(clips) {
    if (window.Timeline && tlRef.current) {
      window.Timeline.render(clips || [], { fps: 59.94 });
    }
  }

  function handleClipsChanged(clips) {
    setCurrentSeq(prev => {
      const next = prev ? { ...prev, clips } : prev;
      currentSeqRef.current = next;
      return next;
    });
    const newHistory = historyRef.current.slice(0, historyIdxRef.current + 1);
    newHistory.push(clips.map(c => ({ ...c })));
    if (newHistory.length > 50) newHistory.shift();
    historyRef.current = newHistory;
    historyIdxRef.current = newHistory.length - 1;
    setHistory(newHistory);
    setHistoryIdx(newHistory.length - 1);
    markDirty();
  }

  function handlePlayheadMoved(frames) {
    setPlayheadFrames(Math.max(0, frames));
  }

  function handleClipContextMenu(payload) {
    setSelectedClipId(payload.clip._id);
    setClipMenu(payload);
  }

  async function handleExternalDrop(payload) {
    const raw = payload.dataTransfer.getData('application/json') || payload.dataTransfer.getData('text/plain');
    let assetId = raw;
    if (!assetId) return;
    try {
      const parsed = JSON.parse(raw);
      assetId = parsed.assetId || parsed.id || assetId;
    } catch (_) {}
    const asset = assetsRef.current.find(function(a) { return a.id === assetId; });
    if (!asset) return;
    let streamUrl = streamCacheRef.current[asset.id];
    if (!streamUrl) {
      try {
        const r = await window.ZAMPP_API.fetch('/assets/' + asset.id + '/stream');
        if (r && r.url) { streamUrl = r.url; streamCacheRef.current[asset.id] = r.url; }
      } catch (e) { window.DF_LOG.warn('[editor] stream URL failed', e); }
    }
    window.Timeline.addClip({ ...asset, streamUrl }, 0, (asset.duration_ms || 10000) / 1000, payload.track, payload.timelineFrames);
  }

  function markDirty() {
    setIsDirty(true);
    isDirtyRef.current = true;
    setSaveStatus('Unsaved');
    clearTimeout(saveTimerRef.current);
    saveTimerRef.current = setTimeout(saveSequence, 2000);
  }

  async function saveSequence() {
    const seq = currentSeqRef.current;
    if (!seq || !isDirtyRef.current) return;
    clearTimeout(saveTimerRef.current);
    setSaveStatus('Saving…');
    try {
      const clips = (seq.clips || []).map(c => ({
        asset_id: c.asset_id,
        track: c.track,
        timeline_in_frames: c.timeline_in_frames,
        timeline_out_frames: c.timeline_out_frames,
        source_in_frames: c.source_in_frames,
        source_out_frames: c.source_out_frames,
      }));
      await window.ZAMPP_API.syncSequenceClips(seq.id, clips);
      if (!mountedRef.current) return;
      setIsDirty(false);
      isDirtyRef.current = false;
      setSaveStatus('Saved');
      if (statusTimerRef.current) clearTimeout(statusTimerRef.current);
      statusTimerRef.current = setTimeout(() => {
        if (mountedRef.current) setSaveStatus('');
      }, 2000);
    } catch (e) {
      if (mountedRef.current) setSaveStatus('Save failed');
      window.DF_LOG.warn('[editor] auto-save failed', e);
    }
  }

  function undo() {
    if (historyIdxRef.current <= 0) return;
    const idx = historyIdxRef.current - 1;
    historyIdxRef.current = idx;
    setHistoryIdx(idx);
    const clips = (historyRef.current[idx] || []).map(c => ({ ...c, _id: _uid() }));
    setCurrentSeq(prev => {
      const next = prev ? { ...prev, clips } : prev;
      currentSeqRef.current = next;
      return next;
    });
    renderTimelineClips(clips);
    markDirty();
  }

  function redo() {
    if (historyIdxRef.current >= historyRef.current.length - 1) return;
    const idx = historyIdxRef.current + 1;
    historyIdxRef.current = idx;
    setHistoryIdx(idx);
    const clips = (historyRef.current[idx] || []).map(c => ({ ...c, _id: _uid() }));
    setCurrentSeq(prev => {
      const next = prev ? { ...prev, clips } : prev;
      currentSeqRef.current = next;
      return next;
    });
    renderTimelineClips(clips);
    markDirty();
  }

  async function loadSourceAsset(asset) {
    setSourceAsset(asset);
    setSrcIn(null);
    setSrcOut(null);
    const vid = srcVideoRef.current;
    if (!vid) return;
    vid.src = '';
    const cache = streamCacheRef.current;
    let url = cache[asset.id];
    if (!url) {
      try {
        const r = await window.ZAMPP_API.fetch('/assets/' + asset.id + '/stream');
        if (r && r.url) { url = r.url; cache[asset.id] = url; }
      } catch (e) { window.DF_LOG.warn('[editor] stream URL failed', e); }
    }
    if (url) { vid.src = url; vid.load(); }
  }

  function markSrcIn() {
    const vid = srcVideoRef.current;
    if (vid) setSrcIn(vid.currentTime);
  }

  function markSrcOut() {
    const vid = srcVideoRef.current;
    if (vid) setSrcOut(vid.currentTime);
  }

  function addToTimeline() {
    if (!sourceAsset || !currentSeq) return;
    if (!window.Timeline) return;
    const vid = srcVideoRef.current;
    const sIn = srcIn != null ? srcIn : 0;
    const sOut = srcOut != null ? srcOut : (vid && vid.duration ? vid.duration : ((sourceAsset.duration_ms || 10000) / 1000));
    window.Timeline.addClip(
      { ...sourceAsset, streamUrl: streamCacheRef.current[sourceAsset.id] },
      sIn, sOut, insertTrack
    );
  }

  function exportEDL() {
    if (!currentSeq) return;
    const name = (currentSeq.name || 'sequence').replace(/[^a-z0-9]/gi, '_') + '.edl';
    window.ZAMPP_API.exportSequenceEDL(currentSeq.id, name).catch(e => window.DF_LOG.warn('[editor] EDL export failed', e));
  }

  function openClipInSource(clip) {
    const asset = assets.find(function(a) { return a.id === clip.asset_id; });
    if (asset) loadSourceAsset(asset);
    setClipMenu(null);
  }

  function deleteClip(clip) {
    const clips = ((currentSeqRef.current && currentSeqRef.current.clips) || []).filter(function(c) { return c._id !== clip._id; });
    setClipMenu(null);
    handleClipsChanged(clips);
    renderTimelineClips(clips);
  }

  function duplicateClip(clip) {
    const duration = clip.timeline_out_frames - clip.timeline_in_frames;
    const dupe = { ...clip, _id: _uid(), timeline_in_frames: clip.timeline_out_frames, timeline_out_frames: clip.timeline_out_frames + duration };
    const clips = [ ...((currentSeqRef.current && currentSeqRef.current.clips) || []), dupe ];
    setClipMenu(null);
    handleClipsChanged(clips);
    renderTimelineClips(clips);
  }

  function splitClipAtPlayhead(clip) {
    const split = playheadFrames;
    if (split <= clip.timeline_in_frames || split >= clip.timeline_out_frames) { setClipMenu(null); return; }
    const offset = split - clip.timeline_in_frames;
    const left = { ...clip, _id: _uid(), timeline_out_frames: split, source_out_frames: clip.source_in_frames + offset };
    const right = { ...clip, _id: _uid(), timeline_in_frames: split, source_in_frames: clip.source_in_frames + offset };
    const clips = ((currentSeqRef.current && currentSeqRef.current.clips) || []).flatMap(function(c) {
      return c._id === clip._id ? [left, right] : [c];
    });
    setClipMenu(null);
    handleClipsChanged(clips);
    renderTimelineClips(clips);
  }

  const srcInLabel = srcIn != null ? (window.TC ? window.TC.framesToTC(window.TC.secondsToFrames(srcIn)) : 'In set') : 'Mark In';
  const srcOutLabel = srcOut != null ? (window.TC ? window.TC.framesToTC(window.TC.secondsToFrames(srcOut)) : 'Out set') : 'Mark Out';

  return (
    <div className="editor-shell" style={{ position: 'relative', display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
      {confirmModal}

      {/* Beta banner: flat strip across top, no glassmorphism or gradient. */}
      <div className="editor-beta-banner">
        <Icon name="editor" size={14} />
        <div className="editor-beta-banner-body">
          <strong>NLE editor is in beta.</strong>
          <span> Use the Premiere Pro panel for frame-accurate editing and growing-file workflows.</span>
        </div>
        <div className="editor-beta-banner-actions">
          <a href={(window.PREMIERE_LATEST || {}).zxp || '#'} download className="btn primary sm">
            Download Premiere panel
          </a>
          <a href={(window.PREMIERE_LATEST || {}).installer || '#'} download className="btn ghost sm">
            Windows installer
          </a>
          <span className="editor-beta-banner-version mono">
            v{(window.PREMIERE_LATEST || {}).version || '·'}
          </span>
        </div>
      </div>

      {/* ── Top toolbar ── */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 12px', borderBottom: '1px solid var(--border)', background: 'var(--bg-panel)', flexShrink: 0, height: 40 }}>
        <Icon name="editor" size={14} />
        {renamingSeq ? (
          <input value={renameVal} autoFocus onChange={e => setRenameVal(e.target.value)}
            onKeyDown={e => { if (e.key === 'Enter') renameSequence(); if (e.key === 'Escape') setRenamingSeq(false); }}
            onBlur={renameSequence}
            style={{ fontSize: 12, height: 24, padding: '0 6px', background: 'var(--bg-surface)', border: '1px solid var(--accent)', borderRadius: 3, color: 'var(--text-primary)', maxWidth: 200 }} />
        ) : (
          <select value={currentSeq ? currentSeq.id : ''} onChange={e => openSequence(e.target.value)}
            style={{ fontSize: 12, height: 24, background: 'var(--bg-surface)', border: '1px solid var(--border)', borderRadius: 3, color: 'var(--text-primary)', maxWidth: 200 }}>
            {sequences.length === 0 && <option value="">No sequences</option>}
            {sequences.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
          </select>
        )}
        <button className="btn ghost sm" style={{ width: 22, height: 22, padding: 0 }} onClick={() => setShowNewSeq(true)} title="New sequence">
          <Icon name="plus" size={12} />
        </button>
        {currentSeq && (
          <button className="btn ghost sm" style={{ width: 22, height: 22, padding: 0 }} title="Rename sequence"
            onClick={() => { setRenameVal(currentSeq.name); setRenamingSeq(true); }}>
            <Icon name="edit" size={12} />
          </button>
        )}
        {currentSeq && (
          <button className="btn ghost sm" style={{ width: 22, height: 22, padding: 0 }} title="Delete sequence"
            onClick={deleteSequence}>
            <Icon name="trash" size={12} />
          </button>
        )}
        <span style={{ width: 1, height: 16, background: 'var(--border)', margin: '0 4px' }} />
        <button className="btn ghost sm" onClick={undo} disabled={historyIdx <= 0} title="Undo (Ctrl+Z)">↩</button>
        <button className="btn ghost sm" onClick={redo} disabled={historyIdx >= history.length - 1} title="Redo (Ctrl+Shift+Z)">↪</button>
        <span style={{ flex: 1 }} />
        <button className="btn ghost sm" onClick={exportEDL} title="Export CMX3600 EDL">Export EDL</button>
        <button className="btn primary sm" onClick={saveSequence}>{saveStatus || 'Save'}</button>
        {saveStatus && <span style={{ fontSize: 10, color: saveStatus === 'Saved' ? 'var(--signal-good)' : 'var(--signal-warn)' }}>{saveStatus}</span>}
      </div>

      {/* ── 2x2 grid ── */}
      <div style={{ flex: 1, display: 'grid', gridTemplateColumns: '1fr 1fr', gridTemplateRows: '35vh 1fr', overflow: 'hidden' }}>
        {/* Source monitor */}
        <div style={{ display: 'flex', flexDirection: 'column', borderRight: '1px solid var(--border)', borderBottom: '1px solid var(--border)', overflow: 'hidden', background: '#000' }}>
          <video ref={srcVideoRef} preload="metadata" style={{ flex: 1, width: '100%', objectFit: 'contain', display: 'block' }} />
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 8px', background: 'var(--bg-panel)', borderTop: '1px solid var(--border)', flexShrink: 0 }}>
            <span style={{ fontSize: 10, fontFamily: 'monospace', color: 'var(--accent)', minWidth: 80 }}>
              {sourceAsset ? (window.TC ? window.TC.framesToTC(window.TC.secondsToFrames(srcVideoRef.current ? srcVideoRef.current.currentTime : 0)) : '00:00:00;00') : '--:--:--;--'}
            </span>
            <button className="btn ghost sm" style={{ fontSize: 10, padding: '2px 6px' }} onClick={markSrcIn} title="Mark In (I)">{srcInLabel}</button>
            <button className="btn ghost sm" style={{ fontSize: 10, padding: '2px 6px' }} onClick={markSrcOut} title="Mark Out (O)">{srcOutLabel}</button>
            <div style={{ flex: 1 }} />
            <select value={insertTrack} onChange={e => setInsertTrack(Number(e.target.value))}
              title="Target track"
              style={{ fontSize: 10, height: 20, background: 'var(--bg-surface)', border: '1px solid var(--border)', borderRadius: 2, color: 'var(--text-secondary)', marginRight: 4 }}>
              <option value={0}>V1</option>
              <option value={1}>V2</option>
              <option value={100}>A1</option>
              <option value={101}>A2</option>
            </select>
            <button className="btn primary sm" style={{ fontSize: 10, padding: '2px 8px' }} onClick={addToTimeline} title="Insert at playhead">Insert</button>
          </div>
        </div>

        {/* Program monitor */}
        <ProgramMonitor
          videoRef={pgmVideoRef}
          currentSeq={currentSeq}
          playheadFrames={playheadFrames}
          setPlayheadFrames={setPlayheadFrames}
          streamCacheRef={streamCacheRef}
        />

        {/* Media panel (bottom left) */}
        <div style={{ display: 'flex', flexDirection: 'column', borderRight: '1px solid var(--border)', overflow: 'hidden', background: 'var(--bg-panel)' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 8px', borderBottom: '1px solid var(--border)', flexShrink: 0 }}>
            <span style={{ fontSize: 10, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--text-tertiary)' }}>Media</span>
            <select value={seqFilter} onChange={e => setSeqFilter(e.target.value)}
              style={{ fontSize: 10, height: 20, marginLeft: 'auto', background: 'var(--bg-surface)', border: '1px solid var(--border)', borderRadius: 2, color: 'var(--text-secondary)' }}>
              <option value="">All bins</option>
              {bins.map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
            </select>
          </div>
          <div style={{ flex: 1, overflowY: 'auto', padding: '4px 0' }}>
            {assets.filter(a => !seqFilter || a.bin_id === seqFilter).length === 0 ? (
              <div style={{ padding: '20px 12px', fontSize: 11, color: 'var(--text-tertiary)', textAlign: 'center' }}>No assets</div>
            ) : assets.filter(a => !seqFilter || a.bin_id === seqFilter).map(a => (
              <div key={a.id} className="media-asset-item"
                draggable="true"
                style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 8px', cursor: 'grab', fontSize: 11, color: 'var(--text-secondary)', borderBottom: '1px solid var(--border-faint)' }}
                onDragStart={function(e) {
                  e.dataTransfer.effectAllowed = 'copy';
                  e.dataTransfer.setData('text/plain', a.id);
                  e.dataTransfer.setData('application/json', JSON.stringify({ assetId: a.id }));
                }}
                onDoubleClick={() => loadSourceAsset(a)}>
                <div style={{ width: 40, height: 24, borderRadius: 2, overflow: 'hidden', flexShrink: 0, background: 'var(--bg-surface)' }}>
                  <AssetThumb asset={a} size="sm" />
                </div>
                <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{a.name}</span>
                <span style={{ fontSize: 9, color: 'var(--text-tertiary)', fontFamily: 'monospace' }}>{a.duration}</span>
              </div>
            ))}
          </div>
        </div>

        {/* Timeline (bottom right) */}
        <div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '4px 8px', borderBottom: '1px solid var(--border)', background: 'var(--bg-panel)', flexShrink: 0 }}>
            <ToolBtn active={tool === 'select'} label="V" title="Select (V)" onClick={() => { setTool('select'); if (window.Timeline) window.Timeline.setTool('select'); }} />
            <ToolBtn active={tool === 'razor'} label="C" title="Razor (C)" onClick={() => { setTool('razor'); if (window.Timeline) window.Timeline.setTool('razor'); }} />
            <ToolBtn active={tool === 'hand'} label="H" title="Hand (H)" onClick={() => { setTool('hand'); if (window.Timeline) window.Timeline.setTool('hand'); }} />
            <span style={{ width: 1, height: 14, background: 'var(--border)', margin: '0 4px' }} />
            <input type="range" min="20" max="500" value={scale} style={{ width: 60, height: 3 }}
              onChange={e => {
                const s = Number(e.target.value);
                setScale(s);
              }} />
            <span style={{ fontSize: 9, color: 'var(--text-tertiary)', fontFamily: 'monospace', width: 28 }}>{scale}px</span>
            <span style={{ marginLeft: 8, fontSize: 10, color: 'var(--text-tertiary)' }}>Drag media here. Right-click clips for actions.</span>
          </div>
          <div ref={tlRef} className="timeline-container" style={{ flex: 1, minHeight: 0 }} />
        </div>
      </div>

      {clipMenu && (
        <ClipContextMenu
          clip={clipMenu.clip}
          x={clipMenu.x}
          y={clipMenu.y}
          onClose={function() { setClipMenu(null); }}
          onOpenSource={openClipInSource}
          onDuplicate={duplicateClip}
          onSplit={splitClipAtPlayhead}
          onDelete={deleteClip}
        />
      )}

      {/* ── New sequence modal ── */}
      {showNewSeq && (
        <div style={{ position: 'fixed', inset: 0, zIndex: 80, background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
          onClick={() => setShowNewSeq(false)}>
          <div style={{ background: 'var(--bg-panel)', border: '1px solid var(--border)', borderRadius: 8, padding: 24, minWidth: 320 }}
            onClick={e => e.stopPropagation()}>
            <div style={{ fontWeight: 600, fontSize: 14, marginBottom: 12 }}>New sequence</div>
            <input value={newSeqName} onChange={e => setNewSeqName(e.target.value)}
              placeholder="Sequence name"
              style={{ width: '100%', height: 32, padding: '0 10px', background: 'var(--bg-deep)', border: '1px solid var(--border)', borderRadius: 4, color: 'var(--text-primary)', fontSize: 13, marginBottom: 12 }}
              onKeyDown={e => { if (e.key === 'Enter') createNewSequence(); }} />
            <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
              <button className="btn ghost sm" onClick={() => setShowNewSeq(false)}>Cancel</button>
              <button className="btn primary sm" onClick={createNewSequence}>Create</button>
            </div>
          </div>
        </div>
      )}

      {/* ── Overlay keyboard handler ── */}
      <EditorKeyboard
        onUndo={undo} onRedo={redo} onSave={saveSequence}
        onMarkIn={markSrcIn} onMarkOut={markSrcOut}
        currentSeq={currentSeq}
      />
    </div>
  );
}

function ToolBtn({ active, label, title, onClick }) {
  return (
    <button onClick={onClick} title={title}
      style={{
        width: 24, height: 22, fontSize: 11, fontWeight: 600,
        background: active ? 'var(--accent-subtle)' : 'var(--bg-surface)',
        border: '1px solid ' + (active ? 'var(--accent-border)' : 'var(--border)'),
        borderRadius: 3, color: active ? 'var(--accent)' : 'var(--text-secondary)',
        cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
      }}>{label}</button>
  );
}

function ClipContextMenu({ clip, x, y, onClose, onOpenSource, onDuplicate, onSplit, onDelete }) {
  const ref = React.useRef(null);
  const [pos, setPos] = React.useState({ left: x, top: y });
  React.useLayoutEffect(() => {
    if (!ref.current) return;
    const r = ref.current.getBoundingClientRect();
    const margin = 8;
    let nx = x, ny = y;
    if (x + r.width + margin > window.innerWidth) nx = window.innerWidth - r.width - margin;
    if (y + r.height + margin > window.innerHeight) ny = window.innerHeight - r.height - margin;
    setPos({ left: Math.max(margin, nx), top: Math.max(margin, ny) });
  }, [x, y]);

  return (
    <div ref={ref} className="ctx-menu" style={{ left: pos.left, top: pos.top }}
      onClick={function(e) { e.stopPropagation(); }}
      onContextMenu={function(e) { e.preventDefault(); e.stopPropagation(); }}>
      <div className="ctx-header">{clip.display_name || clip.name || 'Clip'}</div>
      <button onClick={function() { onClose(); onOpenSource(clip); }}><Icon name="play" size={11} />Load in source</button>
      <button onClick={function() { onClose(); onDuplicate(clip); }}><Icon name="copy" size={11} />Duplicate</button>
      <button onClick={function() { onClose(); onSplit(clip); }}><Icon name="cut" size={11} />Split at playhead</button>
      <div className="ctx-divider" />
      <button className="danger" onClick={function() { onClose(); onDelete(clip); }}><Icon name="trash" size={11} />Remove clip</button>
    </div>
  );
}

function ProgramMonitor({ videoRef, currentSeq, playheadFrames, setPlayheadFrames, streamCacheRef }) {
  const [pgmPlaying, setPgmPlaying] = React.useState(false);
  const [pgmClipIdx, setPgmClipIdx] = React.useState(-1);
  const [pgmClips, setPgmClips] = React.useState([]);

  function getV1Clips() {
    if (!currentSeq || !currentSeq.clips) return [];
    return currentSeq.clips.filter(c => c.track === 0).sort((a, b) => a.timeline_in_frames - b.timeline_in_frames);
  }

  function togglePlay() {
    if (pgmPlaying) {
      stopPgm();
      return;
    }
    const v1 = getV1Clips();
    if (!v1.length) return;
    setPgmClips(v1);
    const idx = v1.findIndex(c => playheadFrames >= c.timeline_in_frames && playheadFrames < c.timeline_out_frames);
    setPgmClipIdx(idx >= 0 ? idx : 0);
    setPgmPlaying(true);
  }

  function stopPgm() {
    setPgmPlaying(false);
    setPgmClipIdx(-1);
    const vid = videoRef.current;
    if (vid) vid.pause();
  }

  React.useEffect(() => {
    if (!pgmPlaying || pgmClipIdx < 0 || pgmClipIdx >= pgmClips.length) return;
    const clip = pgmClips[pgmClipIdx];
    if (!clip) { stopPgm(); return; }
    const vid = videoRef.current;
    if (!vid) return;
    const cache = streamCacheRef.current;
    (async () => {
      let url = cache[clip.asset_id];
      if (!url) {
        try {
          const r = await window.ZAMPP_API.fetch('/assets/' + clip.asset_id + '/stream');
          if (r && r.url) { url = r.url; cache[clip.asset_id] = url; }
        } catch (e) { window.DF_LOG.warn('[editor] stream fetch failed', e); return; }
      }
      if (vid.src !== url) { vid.src = url; vid.load(); }
      const srcInSecs = clip.source_in_frames / (window.TC ? window.TC.FPS : 59.94);
      vid.currentTime = srcInSecs;
      vid.play().catch(() => {});
    })();
  }, [pgmPlaying, pgmClipIdx]);

  function advanceClip() {
    const next = pgmClipIdx + 1;
    if (next >= pgmClips.length) { stopPgm(); return; }
    setPgmClipIdx(next);
  }

  return (
    <div style={{ display: 'flex', flexDirection: 'column', borderBottom: '1px solid var(--border)', overflow: 'hidden', background: '#000' }}>
      <video ref={videoRef} preload="auto" style={{ flex: 1, width: '100%', objectFit: 'contain', display: 'block' }}
        onTimeUpdate={() => {
          if (!pgmPlaying) return;
          const clip = pgmClips[pgmClipIdx];
          if (!clip) return;
          const fps = window.TC ? window.TC.FPS : 59.94;
          const vid = videoRef.current;
          if (!vid) return;
          // Sync timeline playhead to current program position
          const elapsed = vid.currentTime - (clip.source_in_frames / fps);
          const tlFrames = clip.timeline_in_frames + Math.round(elapsed * fps);
          setPlayheadFrames(Math.max(0, tlFrames));
          if (window.Timeline) window.Timeline.setPlayhead(Math.max(0, tlFrames));
          const srcOutSecs = clip.source_out_frames / fps;
          if (vid.currentTime >= srcOutSecs) {
            advanceClip();
          }
        }}
        onEnded={advanceClip}
      />
      <div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 8px', background: 'var(--bg-panel)', borderTop: '1px solid var(--border)', flexShrink: 0 }}>
        <button className="btn ghost sm" style={{ fontSize: 11, padding: '2px 6px' }} onClick={togglePlay}>{pgmPlaying ? '⏸' : '▶'}</button>
        <span style={{ fontSize: 10, fontFamily: 'monospace', color: 'var(--accent)', minWidth: 80 }}>
          {window.TC ? window.TC.framesToTC(playheadFrames) : '00:00:00;00'}
        </span>
      </div>
    </div>
  );
}

function EditorKeyboard({ onUndo, onRedo, onSave, onMarkIn, onMarkOut, currentSeq }) {
  React.useEffect(() => {
    function handler(e) {
      // #116 - `document.activeElement` is null in some edge cases (iframe focus,
      // popovers, devtools-driven focus), and the previous code threw NPE here.
      const tag = (document.activeElement && document.activeElement.tagName) || '';
      if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag)) return;
      if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'z' || e.key === 'Z')) { e.preventDefault(); onRedo(); return; }
      if ((e.ctrlKey || e.metaKey) && (e.key === 'z' || e.key === 'Z')) { e.preventDefault(); onUndo(); return; }
      if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) { e.preventDefault(); onSave(); return; }
      if (e.key === 'i' || e.key === 'I') { onMarkIn(); return; }
      if (e.key === 'o' || e.key === 'O') { onMarkOut(); return; }
      if (e.key === 'v' || e.key === 'V') { if (window.Timeline) window.Timeline.setTool('select'); return; }
      if (e.key === 'c' || e.key === 'C') { if (window.Timeline) window.Timeline.setTool('razor'); return; }
      if (e.key === 'h' || e.key === 'H') { if (window.Timeline) window.Timeline.setTool('hand'); return; }
      if (e.code === 'Space' && !currentSeq) { e.preventDefault(); /* handled by PGM */ }
    }
    document.addEventListener('keydown', handler);
    return () => document.removeEventListener('keydown', handler);
  }, [onUndo, onRedo, onSave, onMarkIn, onMarkOut, currentSeq]);
  return null;
}

window.Editor = Editor;