// screens-asset.jsx — asset detail (Frame.io-style player, filmstrip, comments)

// Simple gradient palette — replaces the missing thumbGrad function
const _FRAME_GRADIENTS = [
  'linear-gradient(135deg,#1a1f2e 0%,#2a3045 100%)',
  'linear-gradient(135deg,#1e2030 0%,#2d2040 100%)',
  'linear-gradient(135deg,#0d1520 0%,#1a2535 100%)',
  'linear-gradient(135deg,#1f1a2e 0%,#2a2040 100%)',
  'linear-gradient(135deg,#0f1e18 0%,#1a3028 100%)',
  'linear-gradient(135deg,#1e1510 0%,#302018 100%)',
  'linear-gradient(135deg,#1a1020 0%,#281830 100%)',
  'linear-gradient(135deg,#101828 0%,#182438 100%)',
  'linear-gradient(135deg,#1e2820 0%,#283830 100%)',
  'linear-gradient(135deg,#201820 0%,#302030 100%)',
  'linear-gradient(135deg,#181e28 0%,#202838 100%)',
];

function AssetDetail({ asset, onClose }) {
  const [playing, setPlaying] = React.useState(false);
  const [currentMs, setCurrentMs] = React.useState(0);
  const [tab, setTab] = React.useState("comments");
  const [showResolved, setShowResolved] = React.useState(false);
  const [comments, setComments] = React.useState([]);
  const [newComment, setNewComment] = React.useState("");
  const [commentsLoading, setCommentsLoading] = React.useState(false);

  // Stream / video state
  const [streamUrl, setStreamUrl] = React.useState(null);
  const [streamType, setStreamType] = React.useState(null);
  // Why the stream is unavailable: 'no_proxy' (has hi-res source, browser
  // can't play it directly) or null (still loading / live / playable).
  const [streamReason, setStreamReason] = React.useState(null);
  const [streamHasSource, setStreamHasSource] = React.useState(false);
  const [streamLoading, setStreamLoading] = React.useState(false);
  const [videoDuration, setVideoDuration] = React.useState(0);
  const [retrying, setRetrying] = React.useState(false);
  const videoRef = React.useRef(null);

  const assetId = asset && asset.id;
  const totalMs = videoDuration > 0 ? videoDuration : parseDuration(asset.duration);

  // Fetch stream URL when asset changes
  React.useEffect(() => {
    if (!assetId) return;
    setStreamUrl(null);
    setStreamType(null);
    setStreamReason(null);
    setStreamHasSource(false);
    setVideoDuration(0);
    setCurrentMs(0);
    setPlaying(false);
    setStreamLoading(true);
    window.ZAMPP_API.fetch('/assets/' + assetId + '/stream')
      .then(function(r) {
        if (r && r.url) {
          setStreamUrl(r.url);
          setStreamType(r.type || 'mp4');
        } else if (r) {
          // {url: null, reason: 'no_proxy', has_source: true|false}
          setStreamReason(r.reason || null);
          setStreamHasSource(!!r.has_source);
        }
      })
      .catch(function() {})
      .finally(function() { setStreamLoading(false); });
  }, [assetId]);

  // Wire hls.js for live HLS streams
  React.useEffect(() => {
    if (!streamUrl || streamType !== 'hls' || !videoRef.current) return;
    if (!window.Hls) return;
    const hls = new window.Hls();
    hls.loadSource(streamUrl);
    hls.attachMedia(videoRef.current);
    return function() { hls.destroy(); };
  }, [streamUrl, streamType]);

  // Fake playback timer — only used when no real video stream
  React.useEffect(() => {
    if (!playing || totalMs <= 0 || streamUrl) return;
    const i = setInterval(function() {
      setCurrentMs(function(t) {
        const next = t + 100;
        if (next >= totalMs) { setPlaying(false); return totalMs; }
        return next;
      });
    }, 100);
    return function() { clearInterval(i); };
  }, [playing, totalMs, streamUrl]);

  const togglePlay = function() {
    if (videoRef.current) {
      if (videoRef.current.paused) { videoRef.current.play(); }
      else { videoRef.current.pause(); }
    } else {
      setPlaying(function(p) { return !p; });
    }
  };

  const seek = function(ms) {
    const clamped = Math.max(0, Math.min(totalMs || 0, ms));
    setCurrentMs(clamped);
    if (videoRef.current) videoRef.current.currentTime = clamped / 1000;
  };

  // Pull a presigned hi-res URL and trigger a browser download with the
  // asset's display name as the filename. Falls back to opening in a new tab.
  const [downloading, setDownloading] = React.useState(false);
  const downloadHires = function() {
    if (downloading) return;
    setDownloading(true);
    window.ZAMPP_API.fetch('/assets/' + assetId + '/hires')
      .then(function(r) {
        if (!r || !r.url) { window.alert('No hi-res source available for this asset.'); return; }
        const a = document.createElement('a');
        a.href = r.url;
        a.download = r.filename || (asset.name + '.' + (r.ext || 'mov'));
        a.target  = '_blank';
        a.rel     = 'noopener';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
      })
      .catch(function(e) { window.alert('Download failed: ' + (e.message || 'unknown error')); })
      .finally(function() { setDownloading(false); });
  };

  // Right-click style menu on the kebab icon — delete, copy ID.
  const [menuOpen, setMenuOpen] = React.useState(false);
  const moreBtnRef = React.useRef(null);
  React.useEffect(function() {
    if (!menuOpen) return;
    const close = function() { setMenuOpen(false); };
    window.addEventListener('click', close);
    return function() { window.removeEventListener('click', close); };
  }, [menuOpen]);

  const copyId = function() {
    if (navigator.clipboard) navigator.clipboard.writeText(assetId).catch(function() {});
    setMenuOpen(false);
  };
  const deleteAsset = function() {
    setMenuOpen(false);
    if (!window.confirm('Delete "' + (asset.name || assetId) + '" permanently?\nRemoves DB row + S3 objects. Cannot be undone.')) return;
    window.ZAMPP_API.fetch('/assets/' + assetId + '?hard=true', { method: 'DELETE' })
      .then(function() { onClose && onClose(); })
      .catch(function(e) { window.alert('Delete failed: ' + e.message); });
  };

  const retryProcessing = function() {
    if (retrying) return;
    setRetrying(true);
    window.ZAMPP_API.fetch('/assets/' + assetId + '/retry', { method: 'POST' })
      .then(function() {
        window.alert('Re-queued for processing. The proxy worker will pick it up shortly; refresh in a minute to see the player.');
      })
      .catch(function(e) { window.alert('Retry failed: ' + (e.message || 'unknown error')); })
      .finally(function() { setRetrying(false); });
  };

  // Map a /assets/:id/comments row into the legacy shape the consumer
  // components (PlaybackBar pins, FilmStrip pins, comment list) already expect.
  function _normalizeComment(row) {
    const frameMs = row.frame_ms != null ? row.frame_ms : 0;
    const real = row.created_at
      ? window.ZAMPP_API.fmtRelative(row.created_at)
      : 'just now';
    return {
      id:       row.id,
      who:      row.author_name || window.ZAMPP_DATA?.ME?.name || 'You',
      avatar:   row.author_initials || (row.author_name ? row.author_name.slice(0, 2).toUpperCase() : (window.ZAMPP_DATA?.ME?.initials || 'ZG')),
      time:     msToTimecode(frameMs),
      frame_ms: frameMs,
      real,
      text:     row.body,
      resolved: !!row.resolved,
      frame:    Math.floor(frameMs / 1000 * 30),
    };
  }

  // Load persisted comments whenever the open asset changes.
  React.useEffect(() => {
    if (!assetId) return;
    setCommentsLoading(true);
    window.ZAMPP_API.fetch('/assets/' + assetId + '/comments')
      .then(function(r) { setComments(((r && r.comments) || []).map(_normalizeComment)); })
      .catch(function() { setComments([]); })
      .finally(function() { setCommentsLoading(false); });
  }, [assetId]);

  const addComment = function() {
    const text = newComment.trim();
    if (!text) return;
    setNewComment('');
    window.ZAMPP_API.fetch('/assets/' + assetId + '/comments', {
      method: 'POST',
      body: JSON.stringify({ body: text, frame_ms: Math.round(currentMs) }),
    })
      .then(function(row) {
        setComments(function(c) { return [...c, _normalizeComment(row)]; });
      })
      .catch(function(e) {
        // Best-effort fallback so the user doesn't lose the typed comment.
        window.alert('Could not post comment: ' + (e.message || 'unknown error'));
        setNewComment(text);
      });
  };

  const toggleResolved = function(c) {
    window.ZAMPP_API.fetch('/assets/' + assetId + '/comments/' + c.id, {
      method: 'PATCH',
      body: JSON.stringify({ resolved: !c.resolved }),
    })
      .then(function(row) {
        setComments(function(prev) { return prev.map(x => x.id === c.id ? _normalizeComment(row) : x); });
      })
      .catch(function() {});
  };

  const deleteComment = function(c) {
    if (!confirm('Delete this comment?')) return;
    window.ZAMPP_API.fetch('/assets/' + assetId + '/comments/' + c.id, { method: 'DELETE' })
      .then(function() {
        setComments(function(prev) { return prev.filter(x => x.id !== c.id); });
      })
      .catch(function(e) { window.alert('Delete failed: ' + e.message); });
  };

  const visibleComments = comments.filter(function(c) { return showResolved || !c.resolved; });

  // The player overlay text reflects three states: live processing,
  // explicit error, "ingested but never proxied" (the SRT-archive case),
  // or generic not-ready.
  const isMissingProxy = !streamUrl && streamReason === 'no_proxy' && streamHasSource;
  const statusMessage =
    asset.status === 'processing' ? 'Processing…' :
    asset.status === 'live'       ? 'Live recording in progress' :
    asset.status === 'error'      ? 'Processing failed' :
    isMissingProxy                ? 'This clip was ingested but never got a browser-playable proxy.' :
                                    'Preview not yet available';

  // "Retry processing" (for status=error) and "Generate proxy" (for
  // status=ready/archived but missing proxy) both call the same /retry
  // endpoint. Label depends on what the user is staring at.
  const canRetry = (asset.status === 'error') || isMissingProxy;
  const retryLabel = isMissingProxy ? 'Generate proxy' : 'Retry processing';

  return (
    <div className="asset-detail fade-in">
      <div className="asset-detail-header">
        <button className="icon-btn" onClick={onClose}><Icon name="arrowLeft" /></button>
        <div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <span style={{ fontWeight: 600, fontSize: 14 }}>{asset.name}</span>
            <StatusDot status={asset.status} />
          </div>
          <div style={{ fontSize: 11.5, color: "var(--text-3)", display: "flex", gap: 6 }}>
            <span>{asset.project}</span><span>·</span><span>updated {asset.updated}</span>
          </div>
        </div>
        <div style={{ flex: 1 }} />
        <button className="btn ghost sm" onClick={downloadHires} disabled={downloading} title="Download the hi-res master file">
          <Icon name="download" />{downloading ? 'Preparing…' : 'Download'}
        </button>
        <div style={{ position: 'relative' }}>
          <button ref={moreBtnRef} className="icon-btn" onClick={function(e) { e.stopPropagation(); setMenuOpen(function(v) { return !v; }); }}>
            <Icon name="more" />
          </button>
          {menuOpen && (
            <div className="row-menu" style={{ right: 0, left: 'auto' }} onClick={function(e) { e.stopPropagation(); }}>
              <button onClick={copyId}><Icon name="library" size={11} />Copy asset ID</button>
              <button className="danger" onClick={deleteAsset}><Icon name="trash" size={11} />Delete permanently</button>
            </div>
          )}
        </div>
      </div>

      <div className="asset-detail-body">
        <div className="player-col">
          <div className="player">
            <div className="player-canvas">
              {streamUrl ? (
                <video
                  key={streamUrl}
                  ref={videoRef}
                  src={streamType !== 'hls' ? streamUrl : undefined}
                  style={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block', background: '#000' }}
                  onTimeUpdate={function() { if (videoRef.current) setCurrentMs(videoRef.current.currentTime * 1000); }}
                  onLoadedMetadata={function() { if (videoRef.current) setVideoDuration(videoRef.current.duration * 1000); }}
                  onPlay={function() { setPlaying(true); }}
                  onPause={function() { setPlaying(false); }}
                  onEnded={function() { setPlaying(false); }}
                />
              ) : (
                <React.Fragment>
                  <FauxFrame seed={asset.seed || 1} />
                  <div className="scanlines" />
                  <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                    <div style={{ textAlign: 'center', color: 'var(--text-3)', maxWidth: 360, padding: '0 16px' }}>
                      {streamLoading ? (
                        <div style={{ fontSize: 13 }}>Loading…</div>
                      ) : (
                        <React.Fragment>
                          <div style={{ fontSize: 13, marginBottom: 8 }}>{statusMessage}</div>
                          <StatusDot status={asset.status} />
                          {canRetry && (
                            <button
                              className="btn primary sm"
                              style={{ marginTop: 14, display: 'inline-flex' }}
                              onClick={retryProcessing}
                              disabled={retrying}
                            >
                              {retrying ? 'Queueing…' : retryLabel}
                            </button>
                          )}
                          {isMissingProxy && (
                            <div style={{ fontSize: 11, color: 'var(--text-4)', marginTop: 10 }}>
                              The hi-res master is in storage. We just need to transcode a browser-playable copy.
                            </div>
                          )}
                        </React.Fragment>
                      )}
                    </div>
                  </div>
                  {!streamLoading && !playing && totalMs > 0 && (
                    <button className="player-play-overlay" onClick={togglePlay}>
                      <Icon name="play" size={28} />
                    </button>
                  )}
                </React.Fragment>
              )}

              <div className="player-overlay-markers">
                {visibleComments
                  .filter(function(c) { return Math.abs(parseDuration(c.time) - currentMs) < 200; })
                  .map(function(c) {
                    return (
                      <div key={c.id} className="player-pin">
                        <div className="player-pin-avatar">{c.avatar}</div>
                        <div className="player-pin-bubble">{c.text}</div>
                      </div>
                    );
                  })}
              </div>
              {asset.status === "live" && (
                <div style={{ position: "absolute", top: 12, left: 12 }}>
                  <span className="badge live">LIVE · REC</span>
                </div>
              )}
              {totalMs > 0 && (
                <div className="player-tc">
                  <span className="mono">{msToTimecode(currentMs)}</span>
                  <span style={{ opacity: 0.5 }}> / {asset.duration}</span>
                </div>
              )}
            </div>

            {totalMs > 0 && (
              <div className="player-controls">
                <button className="icon-btn" onClick={togglePlay}>
                  <Icon name={playing ? "pause" : "play"} size={14} />
                </button>
                <span className="mono" style={{ fontSize: 11.5, color: "var(--text-2)", minWidth: 70 }}>{msToTimecode(currentMs)}</span>
                <PlaybackBar current={currentMs} total={totalMs} onSeek={seek} comments={visibleComments} />
                <span className="mono" style={{ fontSize: 11.5, color: "var(--text-3)", minWidth: 70, textAlign: "right" }}>{asset.duration}</span>
                {streamUrl && (
                  <React.Fragment>
                    <div style={{ width: 1, height: 18, background: "var(--border-strong)" }} />
                    <button
                      className="icon-btn"
                      title="Toggle mute"
                      onClick={function() { if (videoRef.current) { videoRef.current.muted = !videoRef.current.muted; } }}>
                      <Icon name="audio" size={14} />
                    </button>
                    <button
                      className="icon-btn"
                      title="Toggle fullscreen"
                      onClick={function() { if (videoRef.current && videoRef.current.requestFullscreen) videoRef.current.requestFullscreen().catch(function() {}); }}>
                      <Icon name="layout" size={14} />
                    </button>
                  </React.Fragment>
                )}
              </div>
            )}

            {totalMs > 0 && (
              <FilmStrip seed={asset.seed || 1} current={currentMs} total={totalMs} onSeek={seek} comments={visibleComments} />
            )}
          </div>

          <div className="asset-detail-tabs">
            <button className={tab === "comments" ? "active" : ""} onClick={function() { setTab("comments"); }}>
              Comments <span className="count">{comments.length}</span>
            </button>
            <button className={tab === "versions" ? "active" : ""} onClick={function() { setTab("versions"); }}>
              Versions
            </button>
            <button className={tab === "metadata" ? "active" : ""} onClick={function() { setTab("metadata"); }}>
              Metadata
            </button>
            <button className={tab === "audio" ? "active" : ""} onClick={function() { setTab("audio"); }}>
              Audio
            </button>
            <div style={{ flex: 1 }} />
            {tab === "comments" && (
              <label className="tiny-toggle">
                <input type="checkbox" checked={showResolved} onChange={function(e) { setShowResolved(e.target.checked); }} /> Show resolved
              </label>
            )}
          </div>

          <div className="asset-detail-content">
            {tab === "comments" && (
              commentsLoading ? (
                <div style={{ padding: '24px', textAlign: 'center', color: 'var(--text-3)', fontSize: 12.5 }}>Loading comments…</div>
              ) : (
                <CommentsList
                  comments={visibleComments}
                  onSeek={function(c) { seek(c.frame_ms != null ? c.frame_ms : parseDuration(c.time)); }}
                  onResolve={function(id) {
                    const c = comments.find(x => x.id === id);
                    if (c) toggleResolved(c);
                  }}
                  onDelete={function(id) {
                    const c = comments.find(x => x.id === id);
                    if (c) deleteComment(c);
                  }}
                />
              )
            )}
            {tab === "versions" && <VersionsTab />}
            {tab === "metadata" && <MetadataTab asset={asset} />}
            {tab === "audio" && <AudioTab asset={asset} />}
          </div>
        </div>

        <div className="comment-composer-col">
          <CommentComposer
            asset={asset}
            currentMs={currentMs}
            value={newComment}
            onChange={setNewComment}
            onSubmit={addComment}
          />
        </div>
      </div>
    </div>
  );
}

function PlaybackBar({ current, total, onSeek, comments }) {
  const ref = React.useRef(null);
  const handle = function(e) {
    const r = ref.current.getBoundingClientRect();
    const p = (e.clientX - r.left) / r.width;
    onSeek(Math.max(0, Math.min(1, p)) * total);
  };
  const pct = total > 0 ? (current / total) * 100 : 0;
  return (
    <div className="playback-bar" ref={ref} onClick={handle}>
      <div className="playback-fill" style={{ width: pct + '%' }} />
      <div className="playback-handle" style={{ left: pct + '%' }} />
      {comments.map(function(c) {
        const ct = parseDuration(c.time);
        if (!ct || total <= 0) return null;
        const x = (ct / total) * 100;
        return (
          <div
            key={c.id}
            className={'playback-marker ' + (c.resolved ? 'resolved' : '')}
            style={{ left: x + '%' }}
            onClick={function(e) { e.stopPropagation(); onSeek(ct); }}
          >
            <span className="marker-avatar">{c.avatar}</span>
          </div>
        );
      })}
    </div>
  );
}

function FilmStrip({ seed, current, total, onSeek, comments }) {
  const ref = React.useRef(null);
  const frames = 28;
  const handle = function(e) {
    if (!ref.current || total <= 0) return;
    const r = ref.current.getBoundingClientRect();
    onSeek(((e.clientX - r.left) / r.width) * total);
  };
  const pct = total > 0 ? (current / total) * 100 : 0;
  return (
    <div className="filmstrip-wrap">
      <div className="filmstrip" ref={ref} onClick={handle}>
        {Array.from({ length: frames }).map(function(_, i) {
          return (
            <div key={i} className="film-frame" style={{ background: _FRAME_GRADIENTS[(seed + i) % _FRAME_GRADIENTS.length] }}>
              <FauxFrame seed={(seed + i) % 6} />
            </div>
          );
        })}
        <div className="filmstrip-playhead" style={{ left: pct + '%' }} />
        {comments.map(function(c) {
          const ct = parseDuration(c.time);
          if (!ct || total <= 0) return null;
          const x = (ct / total) * 100;
          return (
            <div key={c.id} className={'filmstrip-pin ' + (c.resolved ? 'resolved' : '')} style={{ left: x + '%' }}>
              <span className="filmstrip-pin-avatar">{c.avatar}</span>
            </div>
          );
        })}
      </div>
      <div className="filmstrip-tc">
        {[0, 0.25, 0.5, 0.75, 1].map(function(p) {
          return <span key={p} className="mono">{msToTimecode(p * total)}</span>;
        })}
      </div>
    </div>
  );
}

function CommentsList({ comments, onSeek, onResolve, onDelete }) {
  if (comments.length === 0) {
    return <div style={{ padding: 40, textAlign: "center", color: "var(--text-3)" }}>No comments yet. Add one below to mark a frame.</div>;
  }
  return (
    <div className="comments-list">
      {comments.map(function(c) {
        return (
          <div key={c.id} className={'comment ' + (c.resolved ? 'resolved' : '')}>
            <div className="comment-avatar avatar" style={{ background: avatarColor(c.avatar) }}>{c.avatar}</div>
            <div className="comment-body">
              <div className="comment-head">
                <span className="comment-who">{c.who}</span>
                <button className="comment-time" onClick={function() { onSeek(c); }} title="Jump to this frame"><Icon name="clock" size={10} />{c.time}</button>
                <span className="comment-when">{c.real}</span>
                <div style={{ flex: 1 }} />
                <button className="comment-action" onClick={function() { onResolve(c.id); }}
                  title={c.resolved ? 'Reopen' : 'Mark resolved'}>
                  <Icon name={c.resolved ? "refresh" : "check"} size={12} />
                </button>
                {onDelete && (
                  <button className="comment-action" onClick={function() { onDelete(c.id); }} title="Delete comment">
                    <Icon name="trash" size={12} />
                  </button>
                )}
              </div>
              <div className="comment-text">{c.text}</div>
              {c.resolved && <div className="comment-resolved">✓ Resolved</div>}
            </div>
          </div>
        );
      })}
    </div>
  );
}

function CommentComposer({ asset, currentMs, value, onChange, onSubmit }) {
  const me = window.ZAMPP_DATA?.ME;
  const myInitials = me?.initials || 'ZG';
  return (
    <div className="comment-composer">
      <div className="composer-head">
        <div className="avatar" style={{ background: avatarColor(myInitials), width: 22, height: 22, fontSize: 10 }}>{myInitials}</div>
        <span style={{ fontSize: 12, color: 'var(--text-2)', fontWeight: 500 }}>{me?.name || 'You'}</span>
      </div>
      <div className="composer-tag">
        <span className="badge accent">@ {msToTimecode(currentMs)}</span>
      </div>
      <textarea
        className="composer-input"
        placeholder="Leave a comment at this timecode…"
        value={value}
        onChange={function(e) { onChange(e.target.value); }}
        onKeyDown={function(e) { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) onSubmit(); }}
      />
      <div className="composer-bar">
        <span style={{ flex: 1 }} />
        <span style={{ fontSize: 10.5, color: "var(--text-3)" }}>⌘↵ to send</span>
        <button className="btn primary sm" onClick={onSubmit} disabled={!value.trim()}>Comment</button>
      </div>
    </div>
  );
}

function VersionsTab() {
  return (
    <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-3)', fontSize: 12.5 }}>
      Version history not yet available.
    </div>
  );
}

function MetadataTab({ asset }) {
  const rows = [
    { k: "Filename",   v: asset.name },
    { k: "Duration",   v: asset.duration || '—' },
    { k: "Resolution", v: asset.res || '—' },
    { k: "Codec",      v: asset.codec || '—' },
    { k: "File size",  v: asset.size || '—' },
    { k: "Status",     v: asset.status || '—' },
    { k: "Updated",    v: asset.updated || '—' },
    { k: "Project",    v: asset.project || '—' },
  ];
  return (
    <div className="meta-table">
      {rows.map(function(r) {
        return (
          <div key={r.k} className="meta-row">
            <div className="meta-k">{r.k}</div>
            <div className="meta-v">{r.v}</div>
          </div>
        );
      })}
    </div>
  );
}

function AudioTab({ asset }) {
  return (
    <div style={{ padding: "16px 0" }}>
      <div style={{ background: "var(--bg-2)", border: "1px solid var(--border)", borderRadius: 8, padding: 16, display: "flex", flexDirection: "column", gap: 12 }}>
        <Waveform seed={3} color="var(--accent)" />
        <Waveform seed={7} color="var(--purple)" />
      </div>
    </div>
  );
}

function parseDuration(d) {
  if (!d || d === '—' || typeof d !== 'string') return 0;
  const parts = d.split(':');
  if (parts.length < 2) return 0;
  const nums = parts.map(Number);
  if (nums.some(isNaN)) return 0;
  if (nums.length === 3) return ((nums[0] * 60 + nums[1]) * 60 + nums[2]) * 1000;
  return (nums[0] * 60 + nums[1]) * 1000;
}

function msToTimecode(ms) {
  const total = Math.floor((ms || 0) / 1000);
  const h = Math.floor(total / 3600);
  const m = Math.floor((total % 3600) / 60);
  const s = total % 60;
  return String(h).padStart(2, "0") + ':' + String(m).padStart(2, "0") + ':' + String(s).padStart(2, "0");
}

function avatarColor(initials) {
  let h = 0;
  for (const c of (initials || '?')) h = (h * 31 + c.charCodeAt(0)) & 0xff;
  return 'linear-gradient(135deg, hsl(' + (h % 360) + ' 60% 50%), hsl(' + ((h + 60) % 360) + ' 60% 45%))';
}

Object.assign(window, { AssetDetail, msToTimecode, parseDuration, avatarColor });
