// screens-playout.jsx — Master Control (MCR) playout page.
//
// Redesigned (timeline-centric): styled PGM monitor with audio meters +
// timecode, transport bar, SCTE-35 break panel, now-playing + up-next, a
// horizontal timeline, and a slide-in as-run drawer — all wired to the real
// /api/v1/playout backend (no mock data).
//
// API wiring summary:
//   - Channel CRUD:        GET/POST/DELETE /playout/channels
//   - Channel lifecycle:   POST /channels/:id/start|stop
//   - Engine status:       GET  /channels/:id/status  (polls 4s; carries scteActive)
//   - HLS preview:         GET  /channels/:id/hls/index.m3u8  (hls.js + error recovery)
//   - Playlist CRUD:       GET/POST /playlists, GET/POST/PUT /playlists/:id/items
//   - Transport:           POST /channels/:id/play|pause|resume|skip|stop-playback
//   - As-run log:          GET  /channels/:id/asrun  (polls 5s, drawer)
//   - Staging:             POST /items/:id/stage  (retry)
//   - SCTE-35:             GET/POST /channels/:id/scte, POST /channels/:id/scte/trigger,
//                          DELETE /channels/:id/scte/:id
//
// esbuild bundle:false + jsx:transform — no import statements.
// Globals: React, Icon, window.ZAMPP_API, window.ZAMPP_DATA, window.Hls,
//          window.useConfirm/ConfirmModal.

const PO_OUTPUTS = [
  { value: 'srt',      label: 'SRT' },
  { value: 'rtmp',     label: 'RTMP' },
  { value: 'ndi',      label: 'NDI' },
  { value: 'decklink', label: 'SDI (DeckLink)' },
];

const PO_FORMATS = ['1080p5994', '1080i5994', '1080p2997', '720p5994', '1080i50', '1080p25'];

async function poFetch(path, opts) {
  return window.ZAMPP_API.fetch('/playout' + path, opts);
}

// ── Helpers ───────────────────────────────────────────────────────────────────

function playoutFmtDur(secs) {
  if (!secs || secs < 0) return '—';
  const s = Math.floor(secs);
  const h = Math.floor(s / 3600);
  const m = Math.floor((s % 3600) / 60);
  const ss = s % 60;
  const mm = String(m).padStart(2, '0');
  const ssStr = String(ss).padStart(2, '0');
  return h > 0 ? `${h}:${mm}:${ssStr}` : `${m}:${ssStr}`;
}

function playoutFmtTC(secs) {
  // HH:MM:SS:FF at 29.97 (rounded)
  const s = Math.floor(secs);
  const h = Math.floor(s / 3600);
  const m = Math.floor((s % 3600) / 60);
  const ss = s % 60;
  const ff = Math.floor(((secs - s) * 29.97));
  return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(ss).padStart(2,'0')}:${String(ff).padStart(2,'0')}`;
}

function itemEffectiveDuration(it) {
  const total = (it.asset_duration_ms || 0) / 1000;
  const inPt  = it.in_point  != null ? Number(it.in_point)  : 0;
  const outPt = it.out_point != null ? Number(it.out_point) : total;
  return Math.max(0, outPt - inPt);
}

// ── Clock ─────────────────────────────────────────────────────────────────────
function LiveClock() {
  const [time, setTime] = React.useState(new Date());
  React.useEffect(() => {
    const id = setInterval(() => setTime(new Date()), 500);
    return () => clearInterval(id);
  }, []);
  const pad = n => String(n).padStart(2, '0');
  return (
    <span className="po-clock mono">{pad(time.getHours())}:{pad(time.getMinutes())}:{pad(time.getSeconds())}</span>
  );
}

// ── Elapsed timer ─────────────────────────────────────────────────────────────
function useElapsed(startedAt) {
  const [elapsed, setElapsed] = React.useState(0);
  React.useEffect(() => {
    if (!startedAt) { setElapsed(0); return; }
    const base = new Date(startedAt).getTime();
    const tick = () => setElapsed(Math.max(0, (Date.now() - base) / 1000));
    tick();
    const id = setInterval(tick, 100);
    return () => clearInterval(id);
  }, [startedAt]);
  return elapsed;
}

// ── Output-config sub-form ────────────────────────────────────────────────────
function OutputConfigFields({ type, config, onChange }) {
  const set = (k, v) => onChange({ ...config, [k]: v });
  if (type === 'decklink') {
    return (
      <div className="field">
        <label className="field-label">DeckLink device index</label>
        <input className="field-input" type="number" min="1" value={config.device_index || 1}
          onChange={e => set('device_index', parseInt(e.target.value, 10) || 1)} />
      </div>
    );
  }
  if (type === 'ndi') {
    return (
      <div className="field">
        <label className="field-label">NDI source name</label>
        <input className="field-input" value={config.ndi_name || ''} placeholder="DRAGONFLIGHT CH1"
          onChange={e => set('ndi_name', e.target.value)} />
      </div>
    );
  }
  return (
    <React.Fragment>
      <div className="field">
        <label className="field-label">{type.toUpperCase()} URL</label>
        <input className="field-input mono" value={config.url || ''}
          placeholder={type === 'srt' ? 'srt://host:9000' : 'rtmp://host/live'}
          onChange={e => set('url', e.target.value)} />
      </div>
      {type === 'rtmp' && (
        <div className="field">
          <label className="field-label">Stream key</label>
          <input className="field-input mono" value={config.key || ''}
            onChange={e => set('key', e.target.value)} />
        </div>
      )}
      {type === 'srt' && (
        <div className="field">
          <label className="field-label">Latency (ms)</label>
          <input className="field-input" type="number" value={config.latency || 200}
            onChange={e => set('latency', parseInt(e.target.value, 10) || 200)} />
        </div>
      )}
    </React.Fragment>
  );
}

// ── Channel create modal ──────────────────────────────────────────────────────
function ChannelCreate({ onClose, onCreated }) {
  const PROJECTS = window.ZAMPP_DATA?.PROJECTS || [];
  const [name, setName] = React.useState('');
  const [outputType, setOutputType] = React.useState('srt');
  const [config, setConfig] = React.useState({});
  const [videoFormat, setVideoFormat] = React.useState('1080i5994');
  const [projectId, setProjectId] = React.useState(PROJECTS[0]?.id || '');
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState(null);

  const submit = async () => {
    setBusy(true); setErr(null);
    try {
      const ch = await poFetch('/channels', {
        method: 'POST',
        body: JSON.stringify({
          name, output_type: outputType, output_config: config,
          video_format: videoFormat, project_id: projectId || null,
        }),
      });
      onCreated(ch);
    } catch (e) { setErr(e.message || 'Failed to create channel'); }
    finally { setBusy(false); }
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()} style={{ maxWidth: 460 }}>
        <div className="modal-header"><h3>New Playout Channel</h3></div>
        <div className="modal-body">
          <div className="field">
            <label className="field-label">Name</label>
            <input className="field-input" value={name} autoFocus
              onChange={e => setName(e.target.value)} placeholder="Channel 1" />
          </div>
          <div className="field">
            <label className="field-label">Output</label>
            <select className="field-input" value={outputType}
              onChange={e => { setOutputType(e.target.value); setConfig({}); }}>
              {PO_OUTPUTS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
            </select>
          </div>
          <OutputConfigFields type={outputType} config={config} onChange={setConfig} />
          <div className="field">
            <label className="field-label">Video format</label>
            <select className="field-input" value={videoFormat} onChange={e => setVideoFormat(e.target.value)}>
              {PO_FORMATS.map(f => <option key={f} value={f}>{f}</option>)}
            </select>
          </div>
          <div className="field">
            <label className="field-label">Project (RBAC scope)</label>
            <select className="field-input" value={projectId} onChange={e => setProjectId(e.target.value)}>
              <option value="">— admin only —</option>
              {PROJECTS.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
            </select>
          </div>
          {err && <div className="alert error">{err}</div>}
        </div>
        <div className="modal-footer">
          <button className="btn ghost" onClick={onClose}>Cancel</button>
          <button className="btn primary" disabled={busy || !name} onClick={submit}>
            {busy ? 'Creating…' : 'Create'}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── Media bin ─────────────────────────────────────────────────────────────────
function MediaBin({ projectId }) {
  const ASSETS = (window.ZAMPP_DATA?.ASSETS || []).filter(a =>
    !projectId || a.project_id === projectId);
  const [q, setQ] = React.useState('');
  const filtered = ASSETS.filter(a => !q || (a.name || '').toLowerCase().includes(q.toLowerCase()));

  const onDragStart = (e, asset) => {
    e.dataTransfer.setData('application/x-df-asset', JSON.stringify({ id: asset.id, name: asset.name }));
    e.dataTransfer.effectAllowed = 'copy';
  };

  return (
    <div className="panel po-bin">
      <div className="po-bin-head">
        <span className="po-section-label">Media Bin</span>
        <input className="field-input sm" placeholder="Filter…" value={q}
          onChange={e => setQ(e.target.value)} style={{ maxWidth: 160 }} />
      </div>
      <div className="po-bin-list">
        {filtered.length === 0 && <div className="muted" style={{ padding: 12 }}>No assets.</div>}
        {filtered.map(a => (
          <div key={a.id} className="po-bin-item" draggable
            onDragStart={e => onDragStart(e, a)} title="Drag into the playlist">
            <span className="po-bin-name">{a.name}</span>
            <span className="mono muted" style={{ fontSize: 11 }}>{a.duration || ''}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

// ── Staging bar ───────────────────────────────────────────────────────────────
function StagingBar({ status }) {
  return (
    <div className={'po-staging-bar po-staging-bar--' + (status || 'pending')} aria-hidden="true" />
  );
}

// ── Playlist ──────────────────────────────────────────────────────────────────
function Playlist({ channel, playlistId, items, activeIndex, onReload }) {
  const [dragIndex, setDragIndex] = React.useState(null);
  const [dropErr, setDropErr] = React.useState(null);

  const onItemDragStart = (e, index) => {
    setDragIndex(index);
    e.dataTransfer.effectAllowed = 'move';
  };
  const onItemDragOver = (e) => { e.preventDefault(); };

  const onItemDrop = async (e, index) => {
    e.preventDefault();
    e.stopPropagation();
    setDropErr(null);
    const assetRaw = e.dataTransfer.getData('application/x-df-asset');
    if (assetRaw) {
      try {
        const asset = JSON.parse(assetRaw);
        await poFetch('/playlists/' + playlistId + '/items', {
          method: 'POST', body: JSON.stringify({ asset_id: asset.id }),
        });
        onReload();
      } catch (err) { setDropErr(err.message || 'Failed to add clip'); }
      return;
    }
    if (dragIndex === null || dragIndex === index) return;
    const order = items.map(i => i.id);
    const [moved] = order.splice(dragIndex, 1);
    order.splice(index, 0, moved);
    setDragIndex(null);
    try {
      await poFetch('/playlists/' + playlistId + '/reorder', {
        method: 'PUT', body: JSON.stringify({ order }),
      });
      onReload();
    } catch (err) { setDropErr(err.message || 'Failed to reorder'); }
  };

  const onContainerDrop = async (e) => {
    const assetRaw = e.dataTransfer.getData('application/x-df-asset');
    if (!assetRaw) return;
    e.preventDefault();
    setDropErr(null);
    try {
      const asset = JSON.parse(assetRaw);
      await poFetch('/playlists/' + playlistId + '/items', {
        method: 'POST', body: JSON.stringify({ asset_id: asset.id }),
      });
      onReload();
    } catch (err) { setDropErr(err.message || 'Failed to add clip'); }
  };

  const removeItem = async (id) => {
    try { await poFetch('/items/' + id, { method: 'DELETE' }); onReload(); }
    catch (err) { setDropErr(err.message || 'Failed to remove'); }
  };
  const restage = async (id) => {
    try { await poFetch('/items/' + id + '/stage', { method: 'POST' }); onReload(); }
    catch (err) { setDropErr(err.message || 'Failed to restage'); }
  };

  const totalSecs = items.reduce((sum, it) => sum + itemEffectiveDuration(it), 0);

  return (
    <div className="panel po-playlist" onDragOver={e => e.preventDefault()} onDrop={onContainerDrop}>
      <div className="po-playlist-head">
        <span className="po-section-label">Playlist</span>
        <span className="mono muted" style={{ fontSize: 11, marginLeft: 'auto' }}>
          {items.length} clip{items.length !== 1 ? 's' : ''} · {playoutFmtDur(totalSecs)}
        </span>
        {dropErr && <span className="po-drop-err">{dropErr}</span>}
      </div>
      {items.length === 0 && (
        <div className="muted po-playlist-empty">Drag clips here to build the playlist.</div>
      )}
      {items.map((it, index) => {
        const isActive = index === activeIndex;
        const dur = itemEffectiveDuration(it);
        return (
          <div key={it.id}
            className={'po-pl-item' + (isActive ? ' po-pl-item--active' : '')}
            draggable
            onDragStart={e => onItemDragStart(e, index)}
            onDragOver={onItemDragOver}
            onDrop={e => onItemDrop(e, index)}>
            <span className="po-pl-index">
              {isActive ? <span className="po-pl-onair">▶</span> : index + 1}
            </span>
            <span className="po-pl-name">{it.clip_name || it.asset_id}</span>
            <span className="mono po-pl-dur">{playoutFmtDur(dur)}</span>
            <span className={'badge po-pl-badge ' + (
              it.media_status === 'ready'   ? 'success' :
              it.media_status === 'staging' ? 'warn' :
              it.media_status === 'error'   ? 'error' : 'neutral')}>
              {it.media_status}
            </span>
            {it.media_status === 'error' && (
              <button className="btn ghost xs" onClick={() => restage(it.id)}>Retry</button>
            )}
            <button className="btn ghost xs" onClick={() => removeItem(it.id)}>✕</button>
            <StagingBar status={it.media_status} />
          </div>
        );
      })}
    </div>
  );
}

// ── Audio meter ───────────────────────────────────────────────────────────────
// Simulated VU meter — real values would require a WebAudio analyzer on the
// HLS stream. For now, animate a plausible signal when on-air. (Named PoAudioMeter
// to avoid colliding with the global AudioMeter from visuals.jsx.)
function PoAudioMeter({ onAir }) {
  const canvasRef = React.useRef(null);
  const rafRef = React.useRef(null);

  React.useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    let levelL = 0, levelR = 0;
    let peakL = 0, peakR = 0;
    let peakHoldL = 0, peakHoldR = 0;
    let frame = 0;

    const draw = () => {
      frame++;
      const w = canvas.width, h = canvas.height;
      ctx.clearRect(0, 0, w, h);

      if (onAir) {
        const target = 0.55 + Math.sin(frame * 0.07) * 0.25 + (Math.random() - 0.5) * 0.15;
        levelL += (target - levelL) * 0.25;
        levelR += ((target + (Math.random() - 0.5) * 0.08) - levelR) * 0.25;
        levelL = Math.max(0, Math.min(1, levelL));
        levelR = Math.max(0, Math.min(1, levelR));
        peakL = Math.max(peakL * 0.995, levelL);
        peakR = Math.max(peakR * 0.995, levelR);
        if (levelL >= peakHoldL) { peakHoldL = levelL; } else { peakHoldL *= 0.992; }
        if (levelR >= peakHoldR) { peakHoldR = levelR; } else { peakHoldR *= 0.992; }
      } else {
        levelL *= 0.9; levelR *= 0.9;
        peakHoldL *= 0.9; peakHoldR *= 0.9;
      }

      const barW = Math.floor((w - 6) / 2);
      const drawBar = (x, level, peakHold) => {
        const fillH = Math.floor(level * h);
        ctx.fillStyle = 'rgba(255,255,255,0.05)';
        ctx.fillRect(x, 0, barW, h);
        const greenH = Math.min(fillH, Math.floor(h * 0.7));
        ctx.fillStyle = '#22c55e';
        ctx.fillRect(x, h - greenH, barW, greenH);
        if (fillH > Math.floor(h * 0.7)) {
          const yw = Math.min(fillH - Math.floor(h * 0.7), Math.floor(h * 0.2));
          ctx.fillStyle = '#eab308';
          ctx.fillRect(x, h - Math.floor(h * 0.7) - yw, barW, yw);
        }
        if (fillH > Math.floor(h * 0.9)) {
          const rw = fillH - Math.floor(h * 0.9);
          ctx.fillStyle = '#ef4444';
          ctx.fillRect(x, h - Math.floor(h * 0.9) - rw, barW, rw);
        }
        const peakY = Math.floor((1 - peakHold) * h);
        ctx.fillStyle = peakHold > 0.9 ? '#ef4444' : peakHold > 0.7 ? '#eab308' : '#22c55e';
        ctx.fillRect(x, peakY, barW, 2);
      };
      drawBar(0, levelL, peakHoldL);
      drawBar(barW + 6, levelR, peakHoldR);

      rafRef.current = requestAnimationFrame(draw);
    };

    rafRef.current = requestAnimationFrame(draw);
    return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); };
  }, [onAir]);

  return (
    <canvas ref={canvasRef} className="po-vu-meter" width={38} height={120}
      title="L / R audio levels" />
  );
}

// ── PGM monitor ───────────────────────────────────────────────────────────────
function ProgramMonitor({ channel, engine, elapsed }) {
  const videoRef = React.useRef(null);
  const hlsRef   = React.useRef(null);
  const onAir    = channel.status === 'running';
  // Load the playlist through the API (not the static /media/live path): the
  // public reverse proxy caches the static .m3u8 with a multi-second TTL and
  // ignores no-store, which starved hls.js's reloads of the live edge and kept
  // the monitor black. /api/ isn't proxy-cached, so this always returns fresh.
  const previewUrl = `/api/v1/playout/channels/${channel.id}/hls/index.m3u8`;
  const scte = engine && engine.scteActive;

  React.useEffect(() => {
    const vid = videoRef.current;
    if (!vid) return;

    // Tear down any previous HLS instance before re-evaluating.
    if (hlsRef.current) { hlsRef.current.destroy(); hlsRef.current = null; }
    if (!onAir) { vid.src = ''; return; }

    if (window.Hls && window.Hls.isSupported()) {
      const hls = new window.Hls({
        liveSyncDurationCount: 3,
        liveMaxLatencyDurationCount: 6,
        // Keep hls.js pinned to the live edge. The preview is a CPU-encoded
        // confidence monitor whose live-edge segment may still be mid-write
        // when first fetched; a small back-buffer + tolerant stall handling
        // lets the player skip transient gaps instead of freezing.
        backBufferLength: 8,
        maxBufferLength: 10,
        liveDurationInfinity: true,
        highBufferWatchdogPeriod: 1,
        nudgeMaxRetry: 10,
        // The playlist is served from /api/ (auth-gated); send the session
        // cookie so the request authenticates. Segments are static + public.
        xhrSetup: (xhr) => { xhr.withCredentials = true; },
      });
      hlsRef.current = hls;
      hls.loadSource(previewUrl);
      hls.attachMedia(vid);
      hls.on(window.Hls.Events.MANIFEST_PARSED, () => vid.play().catch(() => {}));

      // Resilient recovery. Without this, the FIRST fatal hls.js error (a
      // buffer stall on the live edge, a media/decode error, or a transient
      // fragment/playlist load error against the rewinding live playlist)
      // permanently halts playback and the monitor goes black — exactly the
      // "flashes a frame then stays black" symptom. We distinguish error types
      // and recover in place rather than tearing down.
      let recoverCount = 0;
      hls.on(window.Hls.Events.ERROR, (_evt, data) => {
        if (!data.fatal) {
          if (data.details === window.Hls.ErrorDetails.BUFFER_STALLED_ERROR) {
            try { hls.startLoad(); } catch (_) {}
          }
          return;
        }
        switch (data.type) {
          case window.Hls.ErrorTypes.NETWORK_ERROR:
            try { hls.startLoad(); } catch (_) {}
            break;
          case window.Hls.ErrorTypes.MEDIA_ERROR:
            recoverCount += 1;
            if (recoverCount <= 3) {
              try { hls.recoverMediaError(); } catch (_) {}
            } else {
              recoverCount = 0;
              try { hls.destroy(); } catch (_) {}
              if (hlsRef.current === hls) hlsRef.current = null;
            }
            break;
          default:
            try { hls.destroy(); } catch (_) {}
            if (hlsRef.current === hls) hlsRef.current = null;
        }
      });
      hls.on(window.Hls.Events.FRAG_BUFFERED, () => {
        if (vid.paused) vid.play().catch(() => {});
      });
    } else if (vid.canPlayType('application/vnd.apple.mpegurl')) {
      vid.src = previewUrl;
      vid.play().catch(() => {});
    }

    return () => {
      if (hlsRef.current) { hlsRef.current.destroy(); hlsRef.current = null; }
    };
  }, [onAir, channel.id]);

  const clipDurSecs = engine && engine.currentItemId
    ? (engine.currentItemDurationMs || 0) / 1000
    : 0;
  const progress = clipDurSecs > 0 ? Math.min(1, elapsed / clipDurSecs) : 0;
  const timeRemaining = clipDurSecs > 0 ? Math.max(0, clipDurSecs - elapsed) : 0;

  // SCTE break countdown (seconds remaining in the active break).
  const breakRemain = scte && scte.endsAt
    ? Math.max(0, (new Date(scte.endsAt).getTime() - Date.now()) / 1000)
    : 0;

  return (
    <div className="po-pgm">
      <div className="po-screen">
        <video ref={videoRef} className="po-monitor-video" muted playsInline autoPlay />

        {/* ON AIR / SCTE BREAK badge */}
        {onAir && scte && (
          <div className="po-onair-badge" style={{ background: '#f59e0b' }}>
            SCTE BREAK{breakRemain > 0 ? ' ' + Math.ceil(breakRemain) + 's' : ''}
          </div>
        )}
        {onAir && !scte && <div className="po-onair-badge">ON AIR</div>}

        {!onAir && (
          <div className="po-screen-offline">
            <span className="po-screen-offline-dot" />
            <span>Channel stopped</span>
          </div>
        )}

        {onAir && (
          <div className="po-tc-overlay mono">{playoutFmtTC(elapsed)}</div>
        )}

        <div className="po-meters-wrap">
          <PoAudioMeter onAir={onAir} />
        </div>
      </div>

      {/* Clip progress bar */}
      <div className="po-clip-progress">
        <div className="po-clip-progress-fill" style={{ width: (progress * 100) + '%' }} />
      </div>

      {/* Monitor footer */}
      <div className="po-monitor-meta">
        <span className="po-monitor-clip-name mono">
          {engine && engine.currentClip
            ? engine.currentClip
            : onAir ? 'Idle' : 'Stopped'}
        </span>
        <span className="po-monitor-right mono muted">
          {engine && engine.currentIndex >= 0 && (
            <React.Fragment>
              <span className="po-clip-pos">
                {engine.currentIndex + 1}/{engine.playlistLength || 0}
              </span>
              {timeRemaining > 0 && (
                <span className="po-clip-remain" title="Time remaining in clip">
                  -{playoutFmtDur(timeRemaining)}
                </span>
              )}
              {engine.loop && <span title="Loop">↺</span>}
              {engine.lastError && (
                <span style={{ color: 'var(--warning)' }} title={engine.lastError}>⚠</span>
              )}
            </React.Fragment>
          )}
        </span>
      </div>
    </div>
  );
}

// ── Transport bar ─────────────────────────────────────────────────────────────
function Transport({ channel, playlistId, items, onStatus, onError }) {
  const [busy, setBusy] = React.useState(false);
  const act = async (fn) => {
    setBusy(true);
    try { await fn(); } catch (e) { onError && onError(e.message); }
    finally { setBusy(false); }
  };

  const notReady = items.filter(i => i.media_status !== 'ready').length;
  const canPlay  = channel.status === 'running' && !busy && !!playlistId && notReady === 0;
  const live     = channel.status === 'running';

  const play    = () => act(async () => {
    const r = await poFetch('/channels/' + channel.id + '/play', {
      method: 'POST', body: JSON.stringify({ playlist_id: playlistId }),
    });
    onStatus && onStatus(r);
  });
  const pause   = () => act(() => poFetch('/channels/' + channel.id + '/pause',         { method: 'POST' }));
  const resume  = () => act(() => poFetch('/channels/' + channel.id + '/resume',        { method: 'POST' }));
  const skip    = () => act(() => poFetch('/channels/' + channel.id + '/skip',          { method: 'POST' }));
  const stopPb  = () => act(() => poFetch('/channels/' + channel.id + '/stop-playback', { method: 'POST' }));

  return (
    <div className="po-transport">
      <div className="po-transport-group">
        <button className="po-trans-btn po-trans-stop" disabled={!live || busy} onClick={stopPb} title="Stop playback">
          <span className="po-trans-icon">⏹</span>
        </button>
        <button className="po-trans-btn po-trans-play" disabled={!canPlay} onClick={play}
          title={notReady > 0 ? notReady + ' clip(s) still staging' : 'Play playlist'}>
          {notReady > 0 && live
            ? <React.Fragment><span className="po-trans-icon">⏳</span><span className="po-trans-label">{notReady} staging</span></React.Fragment>
            : <React.Fragment><span className="po-trans-icon">▶</span><span className="po-trans-label">Play</span></React.Fragment>}
        </button>
        <button className="po-trans-btn po-trans-pause" disabled={!live || busy} onClick={pause} title="Pause">
          <span className="po-trans-icon">⏸</span>
        </button>
        <button className="po-trans-btn po-trans-resume" disabled={!live || busy} onClick={resume} title="Resume">
          <span className="po-trans-icon">⏵</span>
        </button>
        <button className="po-trans-btn po-trans-skip" disabled={!live || busy} onClick={skip} title="Skip to next">
          <span className="po-trans-icon">⏭</span>
        </button>
      </div>
    </div>
  );
}

// ── SCTE-35 panel ─────────────────────────────────────────────────────────────
// Wired to the real backend:
//   POST   /channels/:id/scte/trigger  — splice now (immediate ad break)
//   POST   /channels/:id/scte          — schedule a break (at a playlist pos)
//   GET    /channels/:id/scte          — recent breaks
// The active break (with countdown) comes from engine.scteActive on /status.
function Scte35Panel({ channel, engine, breaks, onReload, onError }) {
  const [busy, setBusy] = React.useState(false);
  const live = channel.status === 'running';
  const scte = engine && engine.scteActive;

  const triggerNow = (durationS, type) => async () => {
    setBusy(true);
    try {
      await poFetch('/channels/' + channel.id + '/scte/trigger', {
        method: 'POST', body: JSON.stringify({ type: type || 'immediate', duration_s: durationS }),
      });
      onReload && onReload();
    } catch (e) { onError && onError('SCTE-35: ' + e.message); }
    finally { setBusy(false); }
  };

  const scheduleAfterCurrent = (durationS) => async () => {
    setBusy(true);
    try {
      const pos = (engine && engine.currentIndex >= 0) ? engine.currentIndex : 0;
      await poFetch('/channels/' + channel.id + '/scte', {
        method: 'POST',
        body: JSON.stringify({ type: 'splice_insert', duration_s: durationS, playlist_pos: pos }),
      });
      onReload && onReload();
    } catch (e) { onError && onError('SCTE-35: ' + e.message); }
    finally { setBusy(false); }
  };

  const lastFired = (breaks || []).find(b => b.fired_at) || null;
  const pending = (breaks || []).filter(b => b.status === 'pending');
  const breakRemain = scte && scte.endsAt
    ? Math.max(0, Math.ceil((new Date(scte.endsAt).getTime() - Date.now()) / 1000))
    : 0;

  return (
    <div className="po-card po-scte-card">
      <div className="po-card-head">
        <span className="po-section-label">SCTE-35 Break</span>
        {scte
          ? <span className="po-scte-stub-badge" style={{ color: '#f59e0b' }}>● ON AIR</span>
          : pending.length > 0
            ? <span className="po-scte-stub-badge">{pending.length} queued</span>
            : null}
      </div>
      <div className="po-scte-body">
        {scte && (
          <div className="po-scte-active mono" style={{ color: '#f59e0b', fontWeight: 600, fontSize: 12 }}>
            In break · {scte.type} · {breakRemain > 0 ? breakRemain + 's left' : 'awaiting return'}
          </div>
        )}
        <div className="po-section-label" style={{ fontSize: 10 }}>Trigger now</div>
        <div className="po-scte-row">
          <button className="po-fire amber" disabled={busy || !live} onClick={triggerNow(30)}>30s</button>
          <button className="po-fire amber" disabled={busy || !live} onClick={triggerNow(60)}>60s</button>
          <button className="po-fire amber" disabled={busy || !live} onClick={triggerNow(120)}>2m</button>
        </div>
        <div className="po-scte-row">
          <button className="po-fire out" disabled={busy || !live} onClick={triggerNow(0, 'splice_out')}>Splice Out</button>
          <button className="po-fire in" disabled={busy || !live} onClick={triggerNow(0, 'splice_in')}>Return</button>
        </div>
        <div className="po-section-label" style={{ fontSize: 10, marginTop: 4 }}>Schedule after current clip</div>
        <div className="po-scte-row">
          <button className="po-fire" disabled={busy || !live} onClick={scheduleAfterCurrent(30)}>+30s</button>
          <button className="po-fire" disabled={busy || !live} onClick={scheduleAfterCurrent(60)}>+60s</button>
          <button className="po-fire" disabled={busy || !live} onClick={scheduleAfterCurrent(120)}>+2m</button>
        </div>
        {lastFired && (
          <div className="po-scte-last mono">
            Last: {lastFired.type} {lastFired.duration_s > 0 ? lastFired.duration_s + 's' : ''} (event {lastFired.event_id})
          </div>
        )}
      </div>
    </div>
  );
}

// ── Now Playing card ──────────────────────────────────────────────────────────
function NowPlayingCard({ engine, elapsed, items }) {
  if (!engine || engine.currentIndex < 0) {
    return (
      <div className="po-card po-nowplaying-card">
        <div className="po-card-head">
          <span className="po-section-label">Now Playing</span>
        </div>
        <div className="po-nowplaying-empty muted">Nothing playing</div>
      </div>
    );
  }

  const currentItem = items[engine.currentIndex] || null;
  const clipDurSecs = currentItem ? itemEffectiveDuration(currentItem) : 0;
  const progress = clipDurSecs > 0 ? Math.min(1, elapsed / clipDurSecs) : 0;
  const timeRemaining = clipDurSecs > 0 ? Math.max(0, clipDurSecs - elapsed) : 0;
  const nextItem = items[engine.currentIndex + 1] || null;

  return (
    <div className="po-card po-nowplaying-card">
      <div className="po-card-head">
        <span className="po-section-label">Now Playing</span>
        <span className="po-nowplaying-pos mono muted">
          {engine.currentIndex + 1} / {engine.playlistLength || items.length}
        </span>
      </div>
      <div className="po-nowplaying-name">{engine.currentClip || '—'}</div>
      <div className="po-nowplaying-progress">
        <div className="po-nowplaying-bar">
          <div className="po-nowplaying-fill" style={{ width: (progress * 100) + '%' }} />
        </div>
        <div className="po-nowplaying-times mono">
          <span className="po-nowplaying-elapsed">{playoutFmtDur(elapsed)}</span>
          <span className="po-nowplaying-remain muted">-{playoutFmtDur(timeRemaining)}</span>
        </div>
      </div>
      {nextItem && (
        <div className="po-nowplaying-next">
          <span className="po-section-label" style={{ fontSize: 10 }}>Up next</span>
          <span className="po-nowplaying-next-name muted">{nextItem.clip_name || nextItem.asset_id}</span>
        </div>
      )}
    </div>
  );
}

// ── Timeline ──────────────────────────────────────────────────────────────────
function Timeline({ items, activeIndex, elapsed, breaks }) {
  const totalSecs = items.reduce((sum, it) => sum + itemEffectiveDuration(it), 0);
  if (items.length === 0) {
    return (
      <div className="po-tl">
        <div className="po-tl-head">
          <span className="po-section-label">Timeline</span>
        </div>
        <div className="po-tl-empty muted">Add clips to the playlist to see the timeline.</div>
      </div>
    );
  }

  let playheadPct = 0;
  if (activeIndex >= 0 && totalSecs > 0) {
    const offsetSecs = items.slice(0, activeIndex).reduce((s, it) => s + itemEffectiveDuration(it), 0);
    const clipDur = itemEffectiveDuration(items[activeIndex] || {});
    playheadPct = ((offsetSecs + Math.min(elapsed, clipDur)) / totalSecs) * 100;
  }

  // Pending position-based breaks → markers at the end of their playlist_pos clip.
  const breakMarkers = [];
  if (totalSecs > 0) {
    for (const b of (breaks || [])) {
      if (b.status !== 'pending' || b.playlist_pos == null) continue;
      const pos = Math.min(b.playlist_pos, items.length - 1);
      const offsetSecs = items.slice(0, pos + 1).reduce((s, it) => s + itemEffectiveDuration(it), 0);
      breakMarkers.push({ id: b.id, pct: (offsetSecs / totalSecs) * 100, dur: b.duration_s });
    }
  }

  const COLORS = ['#3b82f6','#8b5cf6','#06b6d4','#10b981','#f59e0b','#ec4899','#6366f1','#14b8a6'];

  return (
    <div className="po-tl">
      <div className="po-tl-head">
        <span className="po-section-label">Timeline</span>
        <span className="mono muted" style={{ fontSize: 11 }}>{playoutFmtDur(totalSecs)} total</span>
      </div>
      <div className="po-tl-track-wrap">
        {activeIndex >= 0 && (
          <div className="po-tl-playhead" style={{ left: playheadPct + '%' }} />
        )}
        {breakMarkers.map(m => (
          <div key={m.id} className="po-tl-scte-marker" style={{ left: m.pct + '%' }}
            title={'SCTE-35 break · ' + m.dur + 's'} />
        ))}
        <div className="po-tl-track">
          {items.map((it, i) => {
            const dur = itemEffectiveDuration(it);
            const pct = totalSecs > 0 ? (dur / totalSecs) * 100 : 0;
            const isActive = i === activeIndex;
            const color = COLORS[i % COLORS.length];
            return (
              <div key={it.id}
                className={'po-tl-clip' + (isActive ? ' po-tl-clip--active' : '')}
                style={{ width: pct + '%', '--clip-color': color }}
                title={`${it.clip_name || it.asset_id} · ${playoutFmtDur(dur)}`}>
                <span className="po-tl-clip-name">{it.clip_name || it.asset_id}</span>
                <span className="po-tl-clip-dur mono">{playoutFmtDur(dur)}</span>
                {it.media_status === 'staging' && (
                  <span className="po-tl-staging-dot" title="Staging…" />
                )}
                {it.media_status === 'error' && (
                  <span className="po-tl-error-dot" title="Stage error" />
                )}
              </div>
            );
          })}
        </div>
        <div className="po-tl-ruler">
          {totalSecs > 0 && Array.from({ length: 5 }).map((_, i) => (
            <span key={i} className="po-tl-ruler-mark mono" style={{ left: (i * 25) + '%' }}>
              {playoutFmtDur((totalSecs * i) / 4)}
            </span>
          ))}
        </div>
      </div>
    </div>
  );
}

// ── As-run drawer ─────────────────────────────────────────────────────────────
function AsRunDrawer({ channel, refreshKey, open, onClose }) {
  const [rows, setRows] = React.useState([]);

  React.useEffect(() => {
    if (!open) return;
    let alive = true;
    let t;
    const poll = async () => {
      try {
        const r = await poFetch('/channels/' + channel.id + '/asrun');
        if (alive) setRows(Array.isArray(r) ? r : []);
      } catch (_) {}
      t = setTimeout(poll, 5000);
    };
    poll();
    return () => { alive = false; clearTimeout(t); };
  }, [channel.id, refreshKey, open]);

  const fmtTime = (ts) => {
    if (!ts) return '—';
    const d = new Date(ts);
    return isNaN(d) ? '—' : d.toLocaleTimeString();
  };

  return (
    <React.Fragment>
      {open && <div className="po-drawer-backdrop" onClick={onClose} />}
      <div className={'po-drawer' + (open ? ' po-drawer--open' : '')}>
        <div className="po-drawer-head">
          <span className="po-section-label" style={{ fontSize: 13, color: 'var(--text-1)' }}>As-Run Log</span>
          <span className="mono muted" style={{ fontSize: 11, marginLeft: 8 }}>{rows.length} entries</span>
          <button className="btn ghost xs po-drawer-close" onClick={onClose}>✕</button>
        </div>
        <div className="po-drawer-body">
          {rows.length === 0
            ? <div className="mono muted" style={{ padding: '16px 0' }}>No as-run entries yet.</div>
            : (
              <table className="po-asrun-table">
                <thead>
                  <tr><th>Time</th><th>Clip</th><th>Duration</th><th>Result</th></tr>
                </thead>
                <tbody>
                  {rows.slice(0, 200).map((r) => (
                    <tr key={r.id}>
                      <td className="mono">{fmtTime(r.started_at)}</td>
                      <td>{r.clip_name || r.item_id || '—'}</td>
                      <td className="mono">
                        {r.duration_s != null
                          ? playoutFmtDur(Number(r.duration_s))
                          : (r.ended_at ? '—' : 'on air')}
                      </td>
                      <td className={'po-asrun-result po-asrun-' + (r.result || 'played')}>
                        {r.result || 'played'}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            )}
        </div>
      </div>
    </React.Fragment>
  );
}

// ── Channel detail ────────────────────────────────────────────────────────────
function ChannelDetail({ channel, onChannelChange }) {
  const [playlists,  setPlaylists]  = React.useState([]);
  const [playlistId, setPlaylistId] = React.useState(null);
  const [items,      setItems]      = React.useState([]);
  const [engine,     setEngine]     = React.useState(null);
  const [ch,         setCh]         = React.useState(channel);
  const [asRunOpen,  setAsRunOpen]  = React.useState(false);
  const [binOpen,    setBinOpen]    = React.useState(false);
  const [breaks,     setBreaks]     = React.useState([]);
  const [actionErr,  setActionErr]  = React.useState(null);
  const [confirm, confirmModal]     = window.useConfirm();

  React.useEffect(() => { setCh(channel); }, [channel.id]);

  const loadPlaylists = React.useCallback(async () => {
    const pls = await poFetch('/playlists?channel_id=' + channel.id);
    setPlaylists(pls);
    if (pls.length && !playlistId) setPlaylistId(pls[0].id);
    if (!pls.length) {
      const created = await poFetch('/playlists', {
        method: 'POST', body: JSON.stringify({ channel_id: channel.id, name: 'Main' }),
      });
      setPlaylists([created]); setPlaylistId(created.id);
    }
  }, [channel.id]);

  const loadItems = React.useCallback(async () => {
    if (!playlistId) return;
    const its = await poFetch('/playlists/' + playlistId + '/items');
    setItems(its);
  }, [playlistId]);

  const loadBreaks = React.useCallback(async () => {
    try { setBreaks(await poFetch('/channels/' + channel.id + '/scte')); }
    catch (_) { /* table may be empty / migration pending */ }
  }, [channel.id]);

  React.useEffect(() => { loadPlaylists(); loadBreaks(); }, [channel.id]);
  React.useEffect(() => { loadItems(); }, [playlistId]);

  // Poll engine status + item staging + SCTE breaks.
  React.useEffect(() => {
    let t;
    const poll = async () => {
      try {
        const s = await poFetch('/channels/' + channel.id + '/status');
        setEngine(s.engine || null);
      } catch (_) {}
      try { await loadItems(); } catch (_) {}
      try { await loadBreaks(); } catch (_) {}
      t = setTimeout(poll, 4000);
    };
    poll();
    return () => clearTimeout(t);
  }, [channel.id, playlistId]);

  const startChannel = async () => {
    try {
      const updated = await poFetch('/channels/' + channel.id + '/start', { method: 'POST' });
      setCh(updated); onChannelChange(updated);
    } catch (e) { setActionErr(e.message); }
  };
  const stopChannel = async () => {
    try {
      const updated = await poFetch('/channels/' + channel.id + '/stop', { method: 'POST' });
      setCh(updated); onChannelChange(updated);
    } catch (e) { setActionErr(e.message); }
  };
  const deleteChannel = async () => {
    if (!(await confirm({
      title: 'Delete channel?',
      message: 'Delete channel "' + ch.name + '"? This cannot be undone.',
      confirmLabel: 'Delete', danger: true,
    }))) return;
    try {
      await poFetch('/channels/' + ch.id, { method: 'DELETE' });
      onChannelChange({ ...ch, _deleted: true });
    } catch (e) { setActionErr(e.message); }
  };

  const activeIndex = (engine && engine.currentIndex >= 0) ? engine.currentIndex : -1;
  const elapsed = useElapsed(engine && engine.currentItemStartedAt);

  return (
    <div className="po-root">
      {confirmModal}

      {/* ── Top rail: monitor + right panel ── */}
      <div className="po-top">
        <div className="po-pgm-col">
          <ProgramMonitor channel={ch} engine={engine} elapsed={elapsed} />
          <Transport
            channel={ch}
            playlistId={playlistId}
            items={items}
            onStatus={loadItems}
            onError={setActionErr}
          />
        </div>

        <div className="po-rail">
          {/* Channel controls */}
          <div className="po-card po-channel-card">
            <div className="po-card-head">
              <span className="po-section-label">Channel</span>
              <div className="po-channel-actions">
                {ch.status === 'running'
                  ? <button className="btn danger sm" onClick={stopChannel}>Stop</button>
                  : <button className="btn primary sm" onClick={startChannel}>Start</button>}
                {ch.status !== 'running' && (
                  <button className="btn ghost danger xs" onClick={deleteChannel} title="Delete">✕</button>
                )}
              </div>
            </div>
            <div className="po-channel-meta mono muted">
              {ch.output_type?.toUpperCase()} · {ch.video_format}
              {ch.restart_count > 0 && (
                <span className="po-restart-badge" title={'Restarted ' + ch.restart_count + ' time(s)'}>
                  ↺{ch.restart_count}
                </span>
              )}
            </div>
            {ch.error_message && <div className="alert error" style={{ marginTop: 8 }}>{ch.error_message}</div>}
            {actionErr && (
              <div className="alert error" style={{ marginTop: 8 }} onClick={() => setActionErr(null)}>
                {actionErr}
              </div>
            )}
          </div>

          <NowPlayingCard engine={engine} elapsed={elapsed} items={items} />

          <Scte35Panel channel={ch} engine={engine} breaks={breaks}
            onReload={loadBreaks} onError={setActionErr} />

          <div className="po-rail-actions">
            <button className="btn ghost sm po-rail-action-btn" onClick={() => setBinOpen(b => !b)}>
              {binOpen ? '▸ Hide bin' : '▾ Media Bin'}
            </button>
            <button className="btn ghost sm po-rail-action-btn" onClick={() => setAsRunOpen(true)}>
              As-Run Log
            </button>
          </div>
        </div>
      </div>

      {binOpen && <MediaBin projectId={ch.project_id} />}

      {playlistId && (
        <Playlist
          channel={ch}
          playlistId={playlistId}
          items={items}
          activeIndex={activeIndex}
          onReload={loadItems}
        />
      )}

      <Timeline items={items} activeIndex={activeIndex} elapsed={elapsed} breaks={breaks} />

      <AsRunDrawer
        channel={ch}
        refreshKey={engine && engine.currentItemId}
        open={asRunOpen}
        onClose={() => setAsRunOpen(false)}
      />
    </div>
  );
}

// ── Top-level page ────────────────────────────────────────────────────────────
function Playout() {
  const [channels,   setChannels]   = React.useState(null);
  const [selectedId, setSelectedId] = React.useState(null);
  const [showCreate, setShowCreate] = React.useState(false);
  const [err,        setErr]        = React.useState(null);

  const load = React.useCallback(async () => {
    try {
      const list = await poFetch('/channels');
      setChannels(list);
      if (list.length && !selectedId) setSelectedId(list[0].id);
    } catch (e) { setErr(e.message); setChannels([]); }
  }, [selectedId]);

  React.useEffect(() => { load(); }, []);

  const selected = (channels || []).find(c => c.id === selectedId) || null;
  const onChannelChange = (updated) => {
    if (updated._deleted) {
      setChannels(cs => {
        const next = (cs || []).filter(c => c.id !== updated.id);
        setSelectedId(next.length ? next[0].id : null);
        return next;
      });
      return;
    }
    setChannels(cs => (cs || []).map(c => c.id === updated.id ? updated : c));
  };

  return (
    <div className="page">
      {/* ── Page header with channel tabs + clock ── */}
      <div className="po-head">
        <div className="po-head-left">
          <span className="po-head-title">Master Control</span>
          <div className="po-channels-bar">
            {(channels || []).map(c => (
              <button key={c.id}
                className={'po-chan-tab ' + (c.id === selectedId ? 'active' : '')}
                onClick={() => setSelectedId(c.id)}>
                <span className={'po-chan-dot ' + (c.status === 'running' ? 'live' : '')} />
                {c.name}
              </button>
            ))}
            <button className="btn ghost sm" onClick={() => setShowCreate(true)}>+ Channel</button>
          </div>
        </div>
        <LiveClock />
      </div>

      <div className="page-body po-page">
        <div style={{
          background: '#fef3c7',
          borderLeft: '4px solid #f59e0b',
          color: '#78350f',
          padding: '10px 16px',
          fontWeight: 600,
          fontSize: 14,
          borderRadius: 4,
          marginBottom: 12,
          display: 'flex',
          alignItems: 'center',
          gap: 8,
        }}>
          ⚠ Playout is in testing — not for production use.
        </div>
        {err && <div className="alert error">{err}</div>}

        {channels === null && <div className="muted">Loading channels…</div>}
        {channels !== null && channels.length === 0 && (
          <div className="po-empty">
            <p className="muted">No playout channels yet.</p>
            <button className="btn primary" onClick={() => setShowCreate(true)}>
              Create your first channel
            </button>
          </div>
        )}
        {selected && (
          <ChannelDetail
            key={selected.id}
            channel={selected}
            onChannelChange={onChannelChange}
          />
        )}
      </div>

      {showCreate && (
        <ChannelCreate
          onClose={() => setShowCreate(false)}
          onCreated={(ch) => {
            setShowCreate(false);
            setChannels(cs => [...(cs || []), ch]);
            setSelectedId(ch.id);
          }}
        />
      )}
    </div>
  );
}

window.Playout = Playout;
