// Inline watch view + Notifications popover + Add Channel modal
const { useState: useStateW, useEffect: useEffectW, useRef: useRefW } = React;

// ---------- YouTube IFrame API player ----------
// Wraps the YouTube IFrame API. Calls `onTime(sec)` every ~500ms while the
// video has loaded so the rest of the watch view (notes timeline, playhead,
// etc.) stays in sync. The parent passes a `playerRef` to call `.seekTo()`
// when the user clicks a note or scrubs the notes timeline.
function YouTubePlayer({ videoId, onTime, playerRef, onDuration, onEmbedBlocked }) {
  const containerRef = useRefW(null);

  useEffectW(() => {
    if (!videoId || !containerRef.current) return;

    let player = null;
    let pollId = null;
    let cancelled = false;

    function ensureAPI(cb) {
      if (window.YT && window.YT.Player) return cb();
      if (!document.getElementById('yt-iframe-api')) {
        const tag = document.createElement('script');
        tag.id = 'yt-iframe-api';
        tag.src = 'https://www.youtube.com/iframe_api';
        document.head.appendChild(tag);
      }
      const prev = window.onYouTubeIframeAPIReady;
      window.onYouTubeIframeAPIReady = function () {
        if (typeof prev === 'function') prev();
        cb();
      };
      // Fallback polling in case the global hook was already consumed.
      let tries = 0;
      const check = () => {
        if (window.YT && window.YT.Player) cb();
        else if (++tries < 100) setTimeout(check, 100);
      };
      setTimeout(check, 200);
    }

    ensureAPI(() => {
      if (cancelled || !containerRef.current) return;
      try {
        player = new window.YT.Player(containerRef.current, {
          videoId,
          playerVars: { modestbranding: 1, rel: 0, enablejsapi: 1 },
          events: {
            onReady: () => {
              if (cancelled || !player) return;
              try {
                const d = player.getDuration && player.getDuration();
                if (d && onDuration) onDuration(d);
              } catch {}
              pollId = setInterval(() => {
                if (!player || cancelled) return;
                try {
                  const t = player.getCurrentTime && player.getCurrentTime();
                  if (typeof t === 'number' && onTime) onTime(t);
                } catch {}
              }, 500);
            },
            // 2 = invalid id, 100 = not found/private, 101/150 = embed disabled
            onError: (e) => {
              if (cancelled) return;
              if (onEmbedBlocked) onEmbedBlocked(e && e.data);
            },
          },
        });
        if (playerRef) playerRef.current = player;
      } catch {
        // YT.Player construction failed; nothing to clean up beyond the script tag.
      }
    });

    return () => {
      cancelled = true;
      if (pollId) clearInterval(pollId);
      try { if (player && player.destroy) player.destroy(); } catch {}
      if (playerRef) playerRef.current = null;
    };
  }, [videoId]);

  return <div ref={containerRef} className="player-iframe" />;
}

// ---------- Inline tag editor for the watch view ----------
function WatchTags({ channel, state, dispatch }) {
  const [open, setOpen] = useStateW(false);
  const [newTag, setNewTag] = useStateW('');

  const currentTags = channel.tags || [];

  // All tags known across the library minus ones already on this channel
  const available = (() => {
    const set = new Set(state.userTags || []);
    state.channels.forEach(c => (c.tags || []).forEach(t => set.add(t)));
    currentTags.forEach(t => set.delete(t));
    return [...set].sort();
  })();

  const removeTag = (tag) => dispatch({ type: 'remove-tag', channelId: channel.id, tag });
  const addTag = (tag) => dispatch({ type: 'add-tag', channelId: channel.id, tag });

  const addNew = () => {
    const t = newTag.trim().toLowerCase().replace(/^#/, '').replace(/\s+/g, '-');
    if (!t) return;
    addTag(t);
    setNewTag('');
  };

  return (
    <div className="watch-tags">
      <div className="watch-tags-row">
        {currentTags.map(t => (
          <button key={t} className="watch-tag on" onClick={() => removeTag(t)} title="Click to remove">
            <span className="wt-hash">#</span>{t}<span className="wt-x">×</span>
          </button>
        ))}
        <button
          className={`watch-tag-add ${open ? 'open' : ''}`}
          onClick={() => setOpen(o => !o)}
        >
          {open ? '↑ close' : '+ tag channel'}
        </button>
      </div>

      {open && (
        <div className="watch-tags-picker">
          {available.length > 0 && (
            <div className="watch-tags-list">
              {available.map(t => (
                <button key={t} className="watch-tag" onClick={() => addTag(t)}>
                  <span className="wt-hash">#</span>{t}
                </button>
              ))}
            </div>
          )}
          <div className="watch-tags-new">
            <span className="wt-hash">#</span>
            <input
              className="watch-tags-input"
              placeholder="new tag…"
              value={newTag}
              autoFocus={available.length === 0}
              onChange={e => setNewTag(e.target.value)}
              onKeyDown={e => {
                if (e.key === 'Enter') addNew();
                if (e.key === 'Escape') setOpen(false);
              }}
            />
            <button className="watch-tags-create" onClick={addNew} disabled={!newTag.trim()}>
              add
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

function Watch({ state, dispatch }) {
  const { videos, channels, watched, route, focusMode, briefs, notes, playhead, favorites, archived, watchLog, settings } = state;
  const genThumbs = settings?.thumbnails === 'generated';
  const video = videos.find(v => v.id === route.id);
  const channel = channels.find(c => c.id === video?.channelId);
  const ytPlayerRef = useRefW(null);
  const [ytDuration, setYtDuration] = useStateW(0);
  const [embedBlocked, setEmbedBlocked] = useStateW(false);
  const [budgetDismissed, setBudgetDismissed] = useStateW(false);
  const [sideOpen, setSideOpen] = useStateW(true);

  // Attention budget: accumulate real playback time locally, flush to state every ~10s
  // so persistence isn't hammered twice a second by the player poll.
  const lastTickRef = useRefW(null);
  const pendingSecRef = useRefW(0);
  const flushWatch = () => {
    if (pendingSecRef.current >= 1) {
      dispatch({ type: 'log-watch', day: window.SoloUtils.dayKey(), sec: Math.round(pendingSecRef.current) });
      pendingSecRef.current = 0;
    }
  };
  const trackTime = (sec) => {
    const last = lastTickRef.current;
    lastTickRef.current = sec;
    if (last == null) return;
    const delta = sec - last;
    // Only count forward motion at roughly playback speed (ignores seeks/pauses).
    if (delta > 0 && delta < 2) {
      pendingSecRef.current += delta;
      if (pendingSecRef.current >= 10) flushWatch();
    }
  };
  useEffectW(() => {
    lastTickRef.current = null;
    return () => flushWatch(); // flush leftover seconds when leaving the video
  }, [video?.id]);

  const todaySec = watchLog[window.SoloUtils.dayKey()] || 0;
  const limitSec = (settings.dailyLimitMin || 0) * 60;
  const overBudget = limitSec > 0 && todaySec >= limitSec;

  // Auto-mark watched after a short delay (simulating playback)
  useEffectW(() => {
    if (!video) return;
    setYtDuration(0); // reset between videos so stale durations don't leak
    setEmbedBlocked(false); // reset between videos
    if (watched.has(video.id) || video.watched) return;
    const t = setTimeout(() => {
      dispatch({ type: 'mark-watched', id: video.id });
    }, 3500);
    return () => clearTimeout(t);
  }, [video?.id]);

  // Keyboard: F toggles focus, Esc exits focus
  useEffectW(() => {
    function onKey(e) {
      if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) return;
      if (e.key === 'f' || e.key === 'F') { dispatch({ type: 'toggle-focus' }); }
      if (e.key === 'Escape' && focusMode) { dispatch({ type: 'toggle-focus' }); }
    }
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [focusMode]);

  if (!video || !channel) return null;

  const brief = briefs[video.id];
  // Prefer the live YouTube-reported duration when it's available, since the
  // formatted `mm:ss` string we cached at fetch time can drift on edits.
  const totalSec = ytDuration > 0 ? ytDuration : window.SoloUtils.parseDur(video.duration);
  const currentSec = Math.min(totalSec, Math.max(0, playhead[video.id] ?? 0));
  const videoNotes = (notes[video.id] || []).slice().sort((a, b) => {
    if (a.atSec == null && b.atSec == null) return a.createdAt - b.createdAt;
    if (a.atSec == null) return 1;
    if (b.atSec == null) return -1;
    return a.atSec - b.atSec;
  });
  const timedNotes = videoNotes.filter(n => n.atSec != null);

  const seek = (sec) => {
    dispatch({ type: 'seek', videoId: video.id, sec });
    if (video.youtubeId && ytPlayerRef.current && ytPlayerRef.current.seekTo) {
      try { ytPlayerRef.current.seekTo(sec, true); } catch {}
    }
  };

  const requestBrief = async () => {
    if (brief && brief.status === 'loading') return;
    dispatch({ type: 'brief-start', videoId: video.id });
    try {
      const r = await fetch('/api/brief', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          videoTitle: video.title,
          channelName: channel.name,
          channelHandle: channel.handle,
          channelDesc: channel.desc,
          duration: video.duration,
          length: settings.briefLength || 'm',
        }),
      });
      const body = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(body?.error || `Brief failed (${r.status})`);
      dispatch({ type: 'brief-ready', videoId: video.id, text: body.text });
    } catch (e) {
      dispatch({ type: 'brief-error', videoId: video.id, text: String(e?.message || e) });
    }
  };

  const related = videos
    .filter(v => v.channelId === channel.id && v.id !== video.id)
    .sort((a, b) => b.publishedAt - a.publishedAt);

  // Deep zen: focus mode strips everything but the player, title and notes.
  const zen = focusMode && settings.deepZen;
  const isFav = favorites.has(video.id);
  const isArchived = archived ? archived.has(video.id) : false;

  return (
    <main className="main">
      <div className={`watch ${focusMode ? 'focus' : ''} ${!focusMode && !sideOpen ? 'side-collapsed' : ''}`}>
        <div className="watch-head">
          <button className="back" onClick={() => dispatch({ type: 'back' })} title="Back to feed">
            <span className="back-arrow">‹</span>
            <span className="back-label">back to feed</span>
          </button>
          <div className="crumb">
            <span>{channel.name}</span>
            <span className="sep">/</span>
            <span style={{color: 'var(--ink)'}}>now playing</span>
            <span className="sep">/</span>
            <button
              className={`focus-toggle ${focusMode ? 'on' : ''}`}
              onClick={() => dispatch({ type: 'toggle-focus' })}
              title="F · toggle focus mode"
            >
              {focusMode ? '◼ exit focus' : '◻ focus mode'}
            </button>
          </div>
        </div>

        {overBudget && !budgetDismissed && (
          <div className="budget-banner">
            <span className="budget-banner-dot"></span>
            <span className="budget-banner-text">
              You've watched <b>{window.SoloUtils.formatDur(todaySec)}</b> today — past your{' '}
              {settings.dailyLimitMin}m intention. No lock, just a nudge.
            </span>
            <button className="budget-banner-x" onClick={() => setBudgetDismissed(true)}>dismiss</button>
          </div>
        )}

        <div className="watch-main">
          <div className={`player ${video.youtubeId && !embedBlocked ? 'player-real' : ''}`}>
            {video.youtubeId && !embedBlocked ? (
              <YouTubePlayer
                videoId={video.youtubeId}
                playerRef={ytPlayerRef}
                onTime={(sec) => { trackTime(sec); dispatch({ type: 'seek', videoId: video.id, sec }); }}
                onDuration={(d) => setYtDuration(d)}
                onEmbedBlocked={() => setEmbedBlocked(true)}
              />
            ) : embedBlocked ? (
              <div className="player-blocked">
                <window.Thumbnail video={video} channel={channel} large={true} generated={genThumbs} />
                <div className="player-blocked-overlay">
                  <div className="player-blocked-card">
                    <div className="player-blocked-tag">VIDEO UNAVAILABLE</div>
                    <div className="player-blocked-msg">
                      This video can't be embedded — watch it directly on YouTube.
                    </div>
                    <a
                      className="player-blocked-cta"
                      href={`https://www.youtube.com/watch?v=${video.youtubeId}`}
                      target="_blank"
                      rel="noopener noreferrer"
                    >
                      Watch on YouTube ↗
                    </a>
                  </div>
                </div>
              </div>
            ) : (
              <>
                <window.Thumbnail video={video} channel={channel} large={true} generated={genThumbs} />
                <div className="player-play">
                  <div className="tri"></div>
                </div>
                <div className="player-ctrl">
                  <span>{window.SoloUtils.formatDur(currentSec)}</span>
                  <div
                    className="bar"
                    onClick={(e) => {
                      const r = e.currentTarget.getBoundingClientRect();
                      const ratio = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width));
                      seek(ratio * totalSec);
                    }}
                    title="Click to scrub"
                  >
                    <div className="fill" style={{ width: `${(currentSec / totalSec) * 100}%` }}></div>
                    {timedNotes.map(n => (
                      <span key={n.id} className="bar-marker" style={{ left: `${(n.atSec / totalSec) * 100}%` }}></span>
                    ))}
                  </div>
                  <span>{video.duration}</span>
                  <span style={{marginLeft: 12, opacity: 0.7}}>1×</span>
                  <span style={{opacity: 0.7}}>HD</span>
                </div>
              </>
            )}
          </div>

          {/* Mobile action strip — hidden on desktop via CSS */}
          <div className="m-watch-bar">
            <button
              className={`m-watch-act ${isFav ? 'on' : ''}`}
              onClick={() => dispatch({ type: 'toggle-favorite', id: video.id })}
            >{isFav ? '★ saved' : '☆ save'}</button>
            <button
              className="m-watch-act"
              onClick={requestBrief}
              disabled={brief?.status === 'loading'}
            >◎ brief</button>
            <button
              className={`m-watch-act ${(watched.has(video.id) || video.watched) ? 'on' : 'primary'}`}
              onClick={() => {
                if (!(watched.has(video.id) || video.watched)) {
                  dispatch({ type: 'mark-watched', id: video.id });
                }
                dispatch({ type: 'back' });
              }}
            >{(watched.has(video.id) || video.watched) ? '✓ done' : 'mark done'}</button>
          </div>

          <div className="watch-title-row">
            <h1 className="watch-title">{video.title}</h1>
            <div className="watch-title-actions">
              <button
                className={`watch-fav ${isFav ? 'on' : ''}`}
                onClick={() => dispatch({ type: 'toggle-favorite', id: video.id })}
                title={isFav ? 'Remove from favorites' : 'Add to favorites'}
              >{isFav ? '★ favorited' : '☆ favorite'}</button>
              <button
                className={`watch-archive-btn ${isArchived ? 'on' : ''}`}
                onClick={() => dispatch({ type: isArchived ? 'unarchive-video' : 'archive-video', id: video.id })}
                title={isArchived ? 'Unarchive — return to feed' : 'Archive — hide from main feed'}
              >{isArchived ? '⊕ unarchive' : '⊗ archive'}</button>
            </div>
          </div>

          <WatchTags channel={channel} state={state} dispatch={dispatch} />

          {!zen && <div className="watch-stats">
            <div>
              <span className="key">Published</span>
              <span className="val">{window.SoloUtils.timeAgo(video.publishedAt, window.SOLO_DATA.NOW)}</span>
            </div>
            <div>
              <span className="key">Duration</span>
              <span className="val">{video.duration}</span>
            </div>
            <div>
              <span className="key">Views</span>
              <span className="val">{video.views}</span>
            </div>
            <div>
              <span className="key">Status</span>
              <span className="val">{watched.has(video.id) || video.watched ? 'Watched' : 'Now playing'}</span>
            </div>
            <div>
              <span className="key">Notes</span>
              <span className="val">{videoNotes.length}</span>
            </div>
          </div>}

          <window.NotesSection
            video={video}
            notes={videoNotes}
            currentSec={currentSec}
            totalSec={totalSec}
            onSeek={seek}
            onAdd={(text, general) => dispatch({ type: 'add-note', videoId: video.id, atSec: general ? null : Math.floor(currentSec), text })}
            onEdit={(id, text) => dispatch({ type: 'update-note', videoId: video.id, id, text })}
            onRemove={(id) => dispatch({ type: 'remove-note', videoId: video.id, id })}
          />

          {!zen && <window.BriefPanel
            brief={brief}
            onRequest={requestBrief}
            length={settings.briefLength || 'm'}
            onLength={(v) => dispatch({ type: 'set-setting', key: 'briefLength', value: v })}
          />}

          {!zen && <window.SimilarPanel
            video={video}
            onSave={(v) => dispatch({ type: 'open-add', url: `https://www.youtube.com/watch?v=${v.youtubeId}`, intent: 'single' })}
          />}

          {!zen && <div className="watch-channel-card">
            <div className="mark" style={{ background: channel.accent }}>{channel.mark}</div>
            <div className="info">
              <div className="name">{channel.name}</div>
              <div className="sub">{channel.handle} · {channel.subs} subscribers</div>
            </div>
            <button className="btn" onClick={() => dispatch({ type: 'route', route: { type: 'channel', id: channel.id } })}>
              View channel →
            </button>
          </div>}

          {!zen && <div className="watch-desc">
            A patient, slow look at the subject. {channel.desc} For now, SoloTube only shows what you choose to follow — no autoplay, no recommendations.
          </div>}
        </div>

        {!focusMode && (
          <aside className={`watch-side ${sideOpen ? '' : 'collapsed'}`}>
            <div className="watch-side-head">
              {sideOpen && <span>FROM {channel.name.toUpperCase()}</span>}
              {sideOpen && <span>{related.length.toString().padStart(2, '0')}</span>}
              <button
                className="watch-side-toggle"
                onClick={() => setSideOpen(o => !o)}
                title={sideOpen ? 'Collapse' : 'Expand channel list'}
              >{sideOpen ? '›' : '‹'}</button>
            </div>
            {sideOpen && related.map((v, i) => {
              const isWatched = watched.has(v.id) || v.watched;
              return (
                <button
                  key={v.id}
                  className={`v-row-mini ${isWatched ? 'watched' : ''}`}
                  onClick={() => dispatch({ type: 'route', route: { type: 'watch', id: v.id } })}
                >
                  <div className="mini-idx">{String(i+1).padStart(2,'0')}</div>
                  <div className="mini-thumb">
                    <window.Thumbnail video={v} channel={channel} generated={genThumbs} />
                    <div className="v-thumb-dur">{v.duration}</div>
                  </div>
                  <div>
                    <div className="mini-title">{v.title}</div>
                    <div className="mini-sub">{window.SoloUtils.timeAgo(v.publishedAt, window.SOLO_DATA.NOW)}</div>
                  </div>
                </button>
              );
            })}
          </aside>
        )}
      </div>
    </main>
  );
}

// ---------- AI Brief panel ----------
function BriefPanel({ brief, onRequest, length = 'm', onLength }) {
  const status = brief?.status;
  return (
    <div className={`brief ${status || ''}`}>
      <div className="brief-head">
        <div className="brief-eyebrow">
          <span className="brief-dot"></span>
          AI BRIEF · OPTIONAL
        </div>
        {onLength && (
          <div className="brief-len" title="Brief length">
            {['s','m','l'].map(v => (
              <button
                key={v}
                className={`brief-len-btn ${length === v ? 'on' : ''}`}
                onClick={() => onLength(v)}
              >{v.toUpperCase()}</button>
            ))}
          </div>
        )}
        <button
          className={`brief-btn ${status === 'ready' ? 'done' : ''}`}
          onClick={onRequest}
          disabled={status === 'loading'}
        >
          {status === 'loading' ? 'reading…' :
           status === 'ready'   ? 'regenerate' :
           status === 'error'   ? 'retry' :
           '◢ generate brief'}
        </button>
      </div>
      {!status && (
        <div className="brief-empty">
          A short, calm summary of what this video covers — so you can decide if it deserves your attention before pressing play.
        </div>
      )}
      {status === 'loading' && (
        <div className="brief-loading">
          <span className="brief-spinner"></span>
          <span>Composing a brief…</span>
        </div>
      )}
      {status === 'ready' && (
        <pre className="brief-text">{brief.text}</pre>
      )}
      {status === 'error' && (
        <div className="brief-error">
          Couldn't reach the model — {brief.text}
        </div>
      )}
    </div>
  );
}

window.BriefPanel = BriefPanel;

// ---------- Similar Videos panel ----------
function SimilarPanel({ video, onSave }) {
  const [status, setStatus] = useStateW('idle');
  const [videos, setVideos] = useStateW([]);
  const [err, setErr] = useStateW(null);
  const lastId = useRefW(null);

  // Reset when video changes
  useEffectW(() => {
    if (lastId.current !== video.id) {
      lastId.current = video.id;
      setStatus('idle');
      setVideos([]);
      setErr(null);
    }
  }, [video.id]);

  async function run() {
    setStatus('loading');
    setErr(null);
    try {
      const params = new URLSearchParams({
        title: video.title,
        videoId: video.youtubeId || video.id,
      });
      const r = await fetch(`/api/similar?${params}`);
      const body = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(body?.error || `Error ${r.status}`);
      setVideos(body.videos || []);
      setStatus('ready');
    } catch (e) {
      setErr(String(e?.message || e));
      setStatus('error');
    }
  }

  return (
    <div className={`similar ${status}`}>
      <div className="brief-head">
        <div className="brief-eyebrow">
          <span className="brief-dot" style={{ background: '#2563eb' }}></span>
          SIMILAR VIDEOS · ON THIS TOPIC
        </div>
        <button
          className={`brief-btn ${status === 'ready' ? 'done' : ''}`}
          onClick={run}
          disabled={status === 'loading'}
        >
          {status === 'loading' ? 'searching…' :
           status === 'ready'   ? '↻ refresh'  :
           status === 'error'   ? 'retry'       :
           '◢ find similar'}
        </button>
      </div>

      {status === 'idle' && (
        <div className="brief-empty">
          Find videos on the same topic — sourced directly from YouTube, not your feed.
        </div>
      )}
      {status === 'loading' && (
        <div className="brief-loading">
          <span className="brief-spinner"></span>
          <span>Searching for similar videos…</span>
        </div>
      )}
      {status === 'error' && (
        <div className="brief-error">Couldn't load results — {err}</div>
      )}
      {status === 'ready' && (
        <div className="similar-results">
          {videos.length === 0 && (
            <div className="brief-empty">No similar videos found.</div>
          )}
          {videos.map(v => (
            <div key={v.youtubeId} className="similar-item">
              {v.thumb && (
                <img className="similar-thumb" src={v.thumb} alt="" loading="lazy" />
              )}
              <div className="similar-body">
                <div className="similar-title">{v.title}</div>
                <div className="similar-channel">{v.channelName}</div>
              </div>
              <button className="similar-save" onClick={() => onSave(v)}>save →</button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

window.SimilarPanel = SimilarPanel;

// ---------- Notifications popover ----------
function Notifications({ state, dispatch }) {
  const { videos, channels, watched, hidden, notifOpen, notifSeen } = state;
  const ref = useRefW(null);
  const channelsById = {}; for (const c of channels) channelsById[c.id] = c;

  // New = unwatched and recently published (within 14 days)
  const items = videos
    .filter(v => !hidden.has(v.channelId))
    .filter(v => !watched.has(v.id) && !v.watched)
    .filter(v => (window.SOLO_DATA.NOW - v.publishedAt) < 14 * 86400000)
    .sort((a, b) => b.publishedAt - a.publishedAt)
    .slice(0, 8);

  const unseen = items.filter(v => !notifSeen.has(v.id)).length;

  useEffectW(() => {
    function onClick(e) {
      if (!ref.current) return;
      if (!ref.current.contains(e.target) && notifOpen) {
        dispatch({ type: 'notif', open: false });
      }
    }
    window.addEventListener('mousedown', onClick);
    return () => window.removeEventListener('mousedown', onClick);
  }, [notifOpen]);

  return (
    <div ref={ref} style={{position:'relative'}}>
      <button
        className="notif-btn"
        onClick={(e) => { e.stopPropagation(); dispatch({ type: 'notif', open: !notifOpen }); }}
      >
        ◢ Inbox ({items.length.toString().padStart(2,'0')})
        {unseen > 0 && <span className="dot"></span>}
      </button>
      {notifOpen && (
        <div className="notif-pop" onClick={(e)=>e.stopPropagation()}>
          <div className="head">
            <span>NEW · UNWATCHED</span>
            <button style={{fontFamily:'inherit', fontSize:'inherit', letterSpacing:'inherit', color:'var(--ink-3)'}}
                    onClick={() => dispatch({ type: 'notif-mark-seen' })}>
              mark seen
            </button>
          </div>
          {items.length === 0 && <div className="notif-empty">YOU'RE ALL CAUGHT UP.</div>}
          {items.map(v => {
            const c = channelsById[v.channelId];
            return (
              <button key={v.id} className="notif-item"
                onClick={() => {
                  dispatch({ type: 'notif', open: false });
                  dispatch({ type: 'route', route: { type: 'watch', id: v.id } });
                }}>
                <span className="mark" style={{ background: c.accent }}>{c.mark}</span>
                <div className="body">
                  <div className="b1">{c.name}</div>
                  <div className="b2">{v.title}</div>
                </div>
                <span className="time">{window.SoloUtils.timeAgo(v.publishedAt, window.SOLO_DATA.NOW)}</span>
              </button>
            );
          })}
        </div>
      )}
    </div>
  );
}

// ---------- Add Channel modal ----------
function AddChannel({ state, dispatch }) {
  const [step, setStep] = useStateW('paste'); // paste | parsing | preview | fetching | done | error
  // addOpen can carry a prefill URL (e.g. handed off from Scout)
  const [url, setUrl] = useStateW(typeof state.addOpen === 'string' ? state.addOpen : '');
  const [parsed, setParsed] = useStateW(null);
  const [error, setError] = useStateW(null);
  const [selTags, setSelTags] = useStateW([]);
  const [newTagVal, setNewTagVal] = useStateW('');
  const [addType, setAddType] = useStateW(null); // 'single' | 'channel'
  const intent = state.addIntent; // 'single' | 'channel' | null — set by extension deep link

  // Auto-suggested tags: existing tags you already use + keywords found in the
  // channel's own description. You pick; nothing is attached silently.
  const tagSuggestions = (() => {
    if (!parsed) return [];
    const existing = new Set(state.userTags);
    state.channels.forEach(c => (c.tags || []).forEach(t => existing.add(t)));
    const KEYWORDS = ['science','history','math','physics','design','music','tech','engineering',
      'cooking','food','travel','film','essays','documentary','finance','art','gaming','diy',
      'woodworking','photography','space','nature','craft','books','reading','maps','philosophy',
      'architecture','coding','programming','chess','language','fitness','interviews'];
    const hay = ((parsed.desc || '') + ' ' + (parsed.name || '')).toLowerCase();
    const fromDesc = KEYWORDS.filter(k => hay.includes(k));
    return [...new Set([...fromDesc, ...existing])].slice(0, 10);
  })();
  const toggleTag = (t) => setSelTags(s => s.includes(t) ? s.filter(x => x !== t) : [...s, t]);

  // If Scout (or anything else) handed us a URL, resolve it right away.
  useEffectW(() => {
    if (typeof state.addOpen === 'string' && state.addOpen.trim()) startParse();
  }, []);

  // intent=single: auto-confirm as single video once parsed (no modal, go straight to watch)
  // intent=channel: skip two-choice screen — handled by render condition below
  useEffectW(() => {
    if (intent === 'single' && step === 'preview' && parsed?.videoId) {
      confirmSingle();
    }
  }, [intent, step, parsed?.videoId]);

  const startParse = async () => {
    if (!url.trim()) return;
    setError(null);
    setStep('parsing');
    try {
      const r = await fetch('/api/resolve-channel?url=' + encodeURIComponent(url.trim()));
      const body = await r.json();
      if (!r.ok) throw new Error(body?.error || `Lookup failed (${r.status})`);
      setParsed(body);
      setStep('preview');
    } catch (err) {
      setError(String(err?.message || err));
      setStep('error');
    }
  };

  const confirm = async () => {
    if (!parsed) return;
    setAddType('channel');
    setStep('fetching');
    setError(null);
    try {
      const u = new URL('/api/channel-videos', window.location.origin);
      if (parsed.uploadsPlaylistId) u.searchParams.set('uploadsPlaylistId', parsed.uploadsPlaylistId);
      u.searchParams.set('channelId', parsed.id);
      const r = await fetch(u);
      const body = await r.json();
      if (!r.ok) throw new Error(body?.error || `Video fetch failed (${r.status})`);
      dispatch({ type: 'add-channel', channel: { ...parsed, tags: selTags }, videos: body.videos || [] });
      setStep('done');
      setTimeout(() => {
        dispatch({ type: 'close-add' });
        dispatch({ type: 'route', route: { type: 'channel', id: parsed.id } });
      }, 700);
    } catch (err) {
      setError(String(err?.message || err));
      setStep('error');
    }
  };

  // "Just this video" — pull a single video in without following the channel.
  const confirmSingle = async () => {
    if (!parsed?.videoId) return;
    setAddType('single');
    setStep('fetching');
    setError(null);
    try {
      const r = await fetch('/api/video?id=' + encodeURIComponent(parsed.videoId));
      const body = await r.json();
      if (!r.ok) throw new Error(body?.error || `Video fetch failed (${r.status})`);
      dispatch({ type: 'add-single-video', video: body.video, sourceName: parsed.name });
      setStep('done');
      setTimeout(() => {
        dispatch({ type: 'close-add' });
        dispatch({ type: 'route', route: { type: 'watch', id: body.video.id } });
      }, 700);
    } catch (err) {
      setError(String(err?.message || err));
      setStep('error');
    }
  };

  const close = () => dispatch({ type: 'close-add' });

  return (
    <div className="modal-back" onMouseDown={(e) => { if (e.target === e.currentTarget) close(); }}>
      <div className="modal">
        <div className="modal-head">
          <span>+ ADD CHANNEL · STEP {step === 'paste' ? '1/2' : step === 'parsing' ? '1/2' : '2/2'}</span>
          <button className="x" onClick={close}>✕</button>
        </div>
        <div className="modal-body">
          {step === 'paste' && (
            <>
              <div className="modal-step-label">PASTE A YOUTUBE URL</div>
              <h2 className="modal-title">What do you want to follow?</h2>
              <p className="modal-desc">
                Paste a channel URL, a video URL, or a handle. We'll resolve the channel and add it to your feed. We won't suggest anything else.
              </p>
              <input
                className="url"
                autoFocus
                value={url}
                onChange={(e) => setUrl(e.target.value)}
                onKeyDown={(e) => { if (e.key === 'Enter') startParse(); }}
                placeholder="https://www.youtube.com/@somecreator"
              />
              <div className="modal-suggestions">
                <div className="head">OR TRY ONE OF THESE</div>
                {[
                  '@kurzgesagt — In a Nutshell',
                  '@StruthlessOfficial — Tom Pearson',
                  '@RealLifeLore — RLL',
                ].map((s, i) => (
                  <div className="row" key={i} onClick={() => setUrl('https://www.youtube.com/' + s.split(' ')[0])}>
                    <span className="h">↑</span><span>{s}</span>
                  </div>
                ))}
              </div>
            </>
          )}

          {(step === 'parsing' || step === 'fetching') && (
            <div style={{padding: '40px 0', textAlign: 'center'}}>
              <div style={{fontFamily:'var(--ff-mono)', fontSize: 11, letterSpacing: '0.14em', color: 'var(--ink-3)', textTransform: 'uppercase'}}>
                {step === 'parsing' ? 'Resolving channel…' : 'Loading recent videos…'}
              </div>
              <div style={{marginTop: 18, fontFamily:'var(--ff-mono)', fontSize: 12, color: 'var(--ink)'}}>
                {step === 'parsing' ? url : (parsed?.name || '')}
              </div>
              <div style={{marginTop: 24, fontFamily:'var(--ff-mono)', fontSize: 11, color:'var(--ink-3)', letterSpacing:'0.1em'}}>
                [..............]
              </div>
            </div>
          )}

          {step === 'error' && (
            <div style={{padding: '24px 0'}}>
              <div className="modal-step-label" style={{color: 'var(--new)'}}>SOMETHING WENT WRONG</div>
              <h2 className="modal-title">Couldn't add that channel.</h2>
              <p className="modal-desc" style={{whiteSpace:'pre-wrap'}}>{error || 'Unknown error.'}</p>
              <p className="modal-desc" style={{fontSize: 12, color:'var(--ink-3)'}}>
                If this is your first time running locally, make sure <code>YOUTUBE_API_KEY</code> is set in <code>.env</code> and you're running <code>vercel dev</code> (not a plain static server). See <code>.env.example</code>.
              </p>
            </div>
          )}

          {step === 'preview' && parsed && parsed.videoId && !intent && (
            <>
              <div className="modal-step-label">YOU PASTED A VIDEO LINK</div>
              <h2 className="modal-title">What do you want to add?</h2>
              <div className="modal-video-choices">
                <button className="modal-choice" onClick={confirmSingle}>
                  <div className="modal-choice-eyebrow">SAVE THIS VIDEO</div>
                  <div className="modal-choice-name">Just the video</div>
                  <div className="modal-choice-desc">Saved to your feed once. The channel won't appear in your sidebar and future uploads won't come in.</div>
                  <div className="modal-choice-cta">Save video →</div>
                </button>
                <button className="modal-choice" onClick={confirm}>
                  <div className="modal-choice-eyebrow">FOLLOW THE CHANNEL</div>
                  <div className="modal-choice-name">{parsed.name}</div>
                  <div className="modal-choice-desc">{parsed.subs} subscribers · All future uploads will appear in your feed.</div>
                  <div className="modal-choice-cta">Follow channel →</div>
                </button>
              </div>
            </>
          )}

          {step === 'preview' && parsed && (!parsed.videoId || intent === 'channel') && (
            <>
              <div className="modal-step-label">CONFIRM</div>
              <h2 className="modal-title">Add this channel?</h2>
              <div className="parse-preview">
                {parsed.thumbUrl
                  ? <img className="mark mark-img" src={parsed.thumbUrl} alt="" />
                  : <div className="mark" style={{ background: parsed.accent }}>{parsed.mark}</div>}
                <div className="info">
                  <div className="name">{parsed.name}</div>
                  <div className="meta">{parsed.handle} · {parsed.subs} subscribers · joined {parsed.joined}</div>
                </div>
              </div>
              <div className="parse-fields">
                <div className="row"><span className="k">URL</span><span className="v">{url.replace(/^https?:\/\//,'').slice(0,46)}</span></div>
                <div className="row"><span className="k">CHANNEL ID</span><span className="v">{parsed.id}</span></div>
                <div className="row"><span className="k">VIDEOS</span><span className="v">{Number(parsed.videoCount || 0).toLocaleString()}</span></div>
                <div className="row"><span className="k">NOTIFICATIONS</span><span className="v">ON · NEW UPLOADS</span></div>
              </div>
              <div style={{marginTop: 18}}>
                <div className="modal-step-label">TAG IT NOW · OPTIONAL</div>
                {tagSuggestions.length > 0 && (
                  <div className="ch-tags" style={{marginBottom: 10}}>
                    {tagSuggestions.map(t => (
                      <button
                        key={t}
                        className={`tag-chip tag-chip-suggest ${selTags.includes(t) ? 'tag-chip-on' : ''}`}
                        onClick={() => toggleTag(t)}
                      >
                        <span className="hash">#</span>{t}
                        {selTags.includes(t) && <span className="tag-x">✓</span>}
                      </button>
                    ))}
                  </div>
                )}
                <div className="modal-newtag-row">
                  <span className="wt-hash">#</span>
                  <input
                    className="watch-tags-input"
                    placeholder="create new tag…"
                    value={newTagVal}
                    onChange={e => setNewTagVal(e.target.value)}
                    onKeyDown={e => {
                      if (e.key === 'Enter') {
                        const t = newTagVal.trim().toLowerCase().replace(/^#/, '').replace(/\s+/g, '-');
                        if (t && !selTags.includes(t)) setSelTags(s => [...s, t]);
                        setNewTagVal('');
                      }
                    }}
                  />
                  <button
                    className="watch-tags-create"
                    onClick={() => {
                      const t = newTagVal.trim().toLowerCase().replace(/^#/, '').replace(/\s+/g, '-');
                      if (t && !selTags.includes(t)) setSelTags(s => [...s, t]);
                      setNewTagVal('');
                    }}
                    disabled={!newTagVal.trim()}
                  >add</button>
                </div>
                {selTags.length > 0 && (
                  <div className="ch-tags" style={{marginTop: 10}}>
                    {selTags.map(t => (
                      <button
                        key={t}
                        className="tag-chip tag-chip-on"
                        onClick={() => setSelTags(s => s.filter(x => x !== t))}
                        title="Click to remove"
                      >
                        <span className="hash">#</span>{t}
                        <span className="tag-x">×</span>
                      </button>
                    ))}
                  </div>
                )}
              </div>
            </>
          )}

          {step === 'done' && (
            <div style={{padding: '40px 0', textAlign: 'center'}}>
              <div style={{fontFamily:'var(--ff-display)', fontSize: 24, fontWeight: 500, letterSpacing:'-0.01em'}}>
                Added.
              </div>
              <div style={{marginTop: 8, fontFamily:'var(--ff-mono)', fontSize: 11, color:'var(--ink-3)', letterSpacing:'0.08em'}}>
                {addType === 'single' ? 'Taking you to the video…' : 'Taking you to the channel…'}
              </div>
            </div>
          )}
        </div>
        <div className="modal-foot">
          <span>{step === 'paste' && 'ESC to cancel'}</span>
          <div style={{display:'flex', gap:8}}>
            {step === 'paste' && (
              <>
                <button className="btn-ghost" onClick={close}>Cancel</button>
                <button className="btn-primary" disabled={!url.trim()} onClick={startParse}>Continue →</button>
              </>
            )}
            {step === 'preview' && (
              <>
                <button className="btn-ghost" onClick={() => setStep('paste')}>← Back</button>
                {!parsed?.videoId && (
                  <button className="btn-primary" onClick={confirm}>Add to feed</button>
                )}
              </>
            )}
            {step === 'error' && (
              <>
                <button className="btn-ghost" onClick={close}>Cancel</button>
                <button className="btn-primary" onClick={() => { setError(null); setStep('paste'); }}>Try again</button>
              </>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

window.Watch = Watch;
window.Notifications = Notifications;
window.AddChannel = AddChannel;

// ---------- Tag editor modal ----------
function TagEditor({ state, dispatch }) {
  const channel = state.channels.find(c => c.id === state.tagEditor);
  const [val, setVal] = useStateW('');
  if (!channel) return null;

  // Suggest tags already used elsewhere
  const allTags = new Set();
  state.channels.forEach(c => (c.tags || []).forEach(t => allTags.add(t)));
  (channel.tags || []).forEach(t => allTags.delete(t));
  const suggestions = [...allTags].sort();

  const add = (tag) => {
    const t = (tag || val).trim().toLowerCase().replace(/^#/, '').replace(/\s+/g, '-');
    if (!t) return;
    dispatch({ type: 'add-tag', channelId: channel.id, tag: t });
    setVal('');
  };
  const close = () => dispatch({ type: 'close-tag-editor' });

  return (
    <div className="modal-back" onMouseDown={(e) => { if (e.target === e.currentTarget) close(); }}>
      <div className="modal">
        <div className="modal-head">
          <span>EDIT TAGS · {channel.name.toUpperCase()}</span>
          <button className="x" onClick={close}>✕</button>
        </div>
        <div className="modal-body">
          <div className="modal-step-label">TAGS ON THIS CHANNEL</div>
          <div className="ch-tags" style={{marginBottom: 22}}>
            {(channel.tags || []).length === 0 && (
              <span style={{fontFamily:'var(--ff-mono)', fontSize: 11, color:'var(--ink-3)', letterSpacing:'0.06em'}}>
                (none yet)
              </span>
            )}
            {(channel.tags || []).map(t => (
              <button key={t} className="tag-chip"
                onClick={() => dispatch({ type: 'remove-tag', channelId: channel.id, tag: t })}
                title="Click to remove"
              >
                <span className="hash">#</span>{t}
                <span className="tag-x">×</span>
              </button>
            ))}
          </div>

          <div className="modal-step-label">ADD A TAG</div>
          <input
            className="url"
            autoFocus
            value={val}
            onChange={(e) => setVal(e.target.value)}
            onKeyDown={(e) => { if (e.key === 'Enter') add(); }}
            placeholder="e.g. essays, weekend-watch, woodworking"
          />

          {suggestions.length > 0 && (
            <div style={{marginTop: 22}}>
              <div className="modal-step-label">OR PICK AN EXISTING TAG</div>
              <div className="ch-tags">
                {suggestions.map(t => (
                  <button key={t} className="tag-chip tag-chip-suggest" onClick={() => add(t)}>
                    <span className="hash">#</span>{t}
                  </button>
                ))}
              </div>
            </div>
          )}
        </div>
        <div className="modal-foot">
          <span>ENTER to add · ESC to close</span>
          <div style={{display:'flex', gap:8}}>
            <button className="btn-ghost" onClick={close}>Done</button>
            <button className="btn-primary" disabled={!val.trim()} onClick={() => add()}>Add tag</button>
          </div>
        </div>
      </div>
    </div>
  );
}

window.TagEditor = TagEditor;

// ---------- Notes Section: timeline + list + add ----------
function NotesSection({ video, notes, currentSec, totalSec, onSeek, onAdd, onEdit, onRemove }) {
  const [val, setVal] = useStateW('');
  const [general, setGeneral] = useStateW(false); // untimed "scratchpad" note vs pinned to playhead
  const [editingId, setEditingId] = useStateW(null);
  const [editVal, setEditVal] = useStateW('');
  const [hoverNoteId, setHoverNoteId] = useStateW(null);
  const trackRef = useRefW(null);

  const timed = notes.filter(n => n.atSec != null);

  const submit = () => {
    const t = val.trim();
    if (!t) return;
    onAdd(t, general);
    setVal('');
  };

  const seekFromClick = (e) => {
    const r = e.currentTarget.getBoundingClientRect();
    const ratio = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width));
    onSeek(ratio * totalSec);
  };

  const fmt = window.SoloUtils.formatDur;

  // 10 evenly-spaced ticks for the timeline ruler
  const ticks = Array.from({ length: 11 }, (_, i) => i / 10);

  return (
    <section className="notes">
      <div className="notes-head">
        <div className="notes-eyebrow">
          <span className="brief-dot" style={{background: 'var(--ink)'}}></span>
          NOTES · {notes.length} ON THIS VIDEO
        </div>
        <div className="notes-actions">
          <span className="notes-position">
            playhead <b>{fmt(currentSec)}</b> / {fmt(totalSec)}
          </span>
        </div>
      </div>

      <div className="notes-timeline-wrap">
        <div className="notes-timeline-axis">
          <span>0:00</span>
          <span>{fmt(totalSec / 4)}</span>
          <span>{fmt(totalSec / 2)}</span>
          <span>{fmt((totalSec * 3) / 4)}</span>
          <span>{fmt(totalSec)}</span>
        </div>
        <div
          ref={trackRef}
          className="notes-timeline"
          onClick={seekFromClick}
          title="Click anywhere on the timeline to seek"
        >
          {ticks.map((t, i) => (
            <span key={i} className="notes-tick" style={{ left: `${t * 100}%` }}></span>
          ))}
          <div className="notes-played" style={{ width: `${(currentSec / totalSec) * 100}%` }}></div>
          <div className="notes-playhead" style={{ left: `${(currentSec / totalSec) * 100}%` }}></div>

          {timed.map((n, i) => (
            <button
              key={n.id}
              className={`notes-marker ${hoverNoteId === n.id ? 'on' : ''}`}
              style={{ left: `${(n.atSec / totalSec) * 100}%` }}
              onClick={(e) => { e.stopPropagation(); onSeek(n.atSec); }}
              onMouseEnter={() => setHoverNoteId(n.id)}
              onMouseLeave={() => setHoverNoteId(null)}
              title={`${fmt(n.atSec)} · ${n.text}`}
            >
              <span className="notes-marker-num">{i + 1}</span>
              <span className="notes-marker-tip">
                <span className="t">{fmt(n.atSec)}</span>
                <span className="b">{n.text.length > 80 ? n.text.slice(0, 80) + '…' : n.text}</span>
              </span>
            </button>
          ))}
        </div>
      </div>

      <div className="notes-input-row">
        <button
          className={`notes-input-stamp ${general ? 'general' : ''}`}
          onClick={() => setGeneral(g => !g)}
          title={general ? 'Switch to a timestamped note' : 'Switch to a general note (no timestamp)'}
        >
          {general ? '— general' : `@ ${fmt(currentSec)}`}
        </button>
        <input
          className="notes-input"
          placeholder={general ? 'General note on this video…' : 'Note at current playhead…'}
          value={val}
          onChange={(e) => setVal(e.target.value)}
          onKeyDown={(e) => { if (e.key === 'Enter') submit(); }}
        />
        <button className="notes-add-btn" onClick={submit} disabled={!val.trim()}>
          + add note
        </button>
      </div>

      <ol className="notes-list">
        {notes.length === 0 && (
          <li className="notes-empty">
            No notes yet. Click anywhere on the timeline above to set the playhead, then drop a thought.
          </li>
        )}
        {notes.map((n, i) => (
          <li key={n.id} className="notes-row">
            <button
              className={`notes-row-stamp ${n.atSec == null ? 'general' : ''}`}
              onClick={() => { if (n.atSec != null) onSeek(n.atSec); }}
              title={n.atSec != null ? 'Jump to this moment' : 'General note'}
            >
              <span className="i">{String(i + 1).padStart(2, '0')}</span>
              <span className="t">{n.atSec != null ? fmt(n.atSec) : 'GEN'}</span>
            </button>
            <div className="notes-row-body">
              {editingId === n.id ? (
                <textarea
                  className="notes-row-edit"
                  autoFocus
                  value={editVal}
                  onChange={(e) => setEditVal(e.target.value)}
                  onBlur={() => { onEdit(n.id, editVal); setEditingId(null); }}
                  onKeyDown={(e) => {
                    if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
                      onEdit(n.id, editVal); setEditingId(null);
                    }
                    if (e.key === 'Escape') setEditingId(null);
                  }}
                />
              ) : (
                <div className="notes-row-text" onClick={() => { setEditingId(n.id); setEditVal(n.text); }}>
                  {n.text}
                </div>
              )}
            </div>
            <div className="notes-row-act">
              <button onClick={() => { setEditingId(n.id); setEditVal(n.text); }} title="Edit">edit</button>
              <button onClick={() => onRemove(n.id)} title="Remove">×</button>
            </div>
          </li>
        ))}
      </ol>
    </section>
  );
}

window.NotesSection = NotesSection;
