/* global React */

// t21 / v06p — Audio page.
//
// TWO sections, repurposed:
//   1. CHARACTER VOICES — a preview row PER character that has an
//      ElevenLabs voice assigned (set on the Assets / Characters
//      modal). Each row plays the cached voice preview audio so
//      Hugo can quickly hear what each character will sound like
//      WITHOUT generating a real shot take.
//   2. NARRATION / VO — all real VO takes generated via the
//      Generate → Voice Over workflow. Grouped by segment
//      (shot id or shot range like "SH0010 → SH0060"), then by
//      character. Multiple versions (v001, v002, …) for the same
//      segment sit together so Hugo can compare takes.
//
// Persistent mini-player at the bottom holds the active track +
// scrub bar + transport controls + volume.

function fmtAudioTime(s) {
  if (!Number.isFinite(s) || s < 0) return "0:00";
  const m = Math.floor(s / 60);
  const sec = Math.floor(s % 60);
  return `${m}:${String(sec).padStart(2, "0")}`;
}

function AudioPage() {
  const fetcher = window.authFetch || fetch;
  const [assets, setAssets] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);

  // Mini-player state. currentTrack is the asset row, queue is the
  // ordered list of remaining tracks after currentTrack.
  const [currentTrack, setCurrentTrack] = React.useState(null);
  const [queue, setQueue] = React.useState([]);
  const [isPlaying, setIsPlaying] = React.useState(false);
  const [progress, setProgress] = React.useState(0);
  const [duration, setDuration] = React.useState(0);
  const [volume, setVolume] = React.useState(0.85);
  const audioRef = React.useRef(null);

  // v06p — Character previews fetched from the in-memory app
  // data. We re-render on changes (so adding a character voice on
  // the Assets page surfaces here immediately).
  const [appDataRev, setAppDataRev] = React.useState(0);
  React.useEffect(() => {
    const bump = () => setAppDataRev(r => r + 1);
    window.addEventListener("paradise-sse", bump);
    return () => window.removeEventListener("paradise-sse", bump);
  }, []);
  const charactersWithVoice = React.useMemo(() => {
    const data = (typeof window !== "undefined" && window.__appData) || {};
    // 24 Sep 2026 (G5) — another project: the characters of ITS character categories.
    const arr = (window.__projectCharacters && window.__isDefaultProject && !window.__isDefaultProject())
      ? window.__projectCharacters()
      : ((data.assets && data.assets.characters) || []);
    return Array.isArray(arr) ? arr.filter(c => c.voice_id) : [];
  }, [appDataRev]);

  const load = React.useCallback(() => {
    setLoading(true); setError(null);
    fetcher("/api/audio")
      .then(r => r.ok ? r.json() : null)
      .then(d => { setAssets((d && d.assets) || []); setLoading(false); })
      .catch(err => { setError(err.message); setLoading(false); });
  }, [fetcher]);
  React.useEffect(() => { load(); }, [load]);

  // v06p — Auto-reload audio list whenever the server broadcasts
  // a new VO take so freshly-generated audio appears without a
  // manual refresh.
  React.useEffect(() => {
    const fn = (e) => {
      try {
        const msg = (e && e.detail) || JSON.parse(e.data || "{}");
        if (msg && msg.type === "audio_asset_added") load();
      } catch (_) {}
    };
    window.addEventListener("paradise-sse", fn);
    return () => window.removeEventListener("paradise-sse", fn);
  }, [load]);

  // ─── Narration grouping (v06p) ────────────────────────────────
  // Every saved VO take (including takes that have a
  // character_name) is treated as NARRATION here — the
  // Character Voices section is now reserved for character
  // previews. We accept legacy `character_voice` rows that have
  // a shot_id too, so old data isn't orphaned.
  const narrationTakes = React.useMemo(() => assets.filter(a =>
    (a.type === "narration") || (a.type === "character_voice" && a.shot_id)
  ), [assets]);

  // Group by (segment_label, character_name). Within a group,
  // takes are sorted newest-first by version number desc.
  const narrationGroups = React.useMemo(() => {
    const map = new Map();
    for (const a of narrationTakes) {
      const seg = a.segment_label || a.shot_id || "(unscoped)";
      const who = a.character_name || "Narration";
      const key = `${seg}::${who}`;
      if (!map.has(key)) map.set(key, { segment: seg, character: who, shot_id: a.shot_id, takes: [] });
      map.get(key).takes.push(a);
    }
    for (const g of map.values()) {
      g.takes.sort((a, b) => {
        const av = parseInt((a.version || "v0").replace(/[^0-9]/g, ""), 10) || 0;
        const bv = parseInt((b.version || "v0").replace(/[^0-9]/g, ""), 10) || 0;
        return bv - av;
      });
    }
    // Order groups: by shot id ascending where possible.
    return [...map.values()].sort((a, b) => {
      const an = parseInt(String(a.shot_id || "").replace(/[^0-9]/g, ""), 10) || 0;
      const bn = parseInt(String(b.shot_id || "").replace(/[^0-9]/g, ""), 10) || 0;
      if (an !== bn) return an - bn;
      return a.character.localeCompare(b.character);
    });
  }, [narrationTakes]);

  // ─── Transport (v06p) ─────────────────────────────────────────
  // Plays a track. If the audio element is already mounted with a
  // different src, switching the src triggers a load → we wait
  // a tick for the new src to settle before calling play().
  const playTrack = (track, after = []) => {
    setCurrentTrack(track);
    setQueue(after);
    setIsPlaying(true);
    setTimeout(() => {
      if (audioRef.current) {
        try { audioRef.current.currentTime = 0; audioRef.current.play(); } catch (e) {}
      }
    }, 30);
  };
  // v06p — Toggle: if the row clicked is ALREADY the active
  // track, pause/resume it instead of restarting. Previously the
  // row's play button always called playTrack(), which reset
  // currentTime to 0 — so the pause button visually toggled but
  // the audio just jumped back to start.
  const togglePlayRow = (track) => {
    if (currentTrack && currentTrack.id === track.id && audioRef.current) {
      if (isPlaying) audioRef.current.pause();
      else audioRef.current.play();
      return;
    }
    playTrack(track);
  };
  const togglePlay = () => {
    if (!currentTrack || !audioRef.current) return;
    if (isPlaying) audioRef.current.pause();
    else audioRef.current.play();
  };
  const next = () => {
    if (queue.length === 0) {
      setIsPlaying(false);
      return;
    }
    const [head, ...rest] = queue;
    playTrack(head, rest);
  };
  const restart = () => {
    if (audioRef.current) {
      try { audioRef.current.currentTime = 0; audioRef.current.play(); } catch (e) {}
    }
  };
  const seekTo = (ratio) => {
    if (!audioRef.current || !duration) return;
    audioRef.current.currentTime = Math.max(0, Math.min(duration, ratio * duration));
  };

  // v06p — Character voice preview. Builds a synthetic "track"
  // pointing at the cached ElevenLabs preview_url (or a server-
  // generated TTS fallback), then pipes it through the same
  // mini-player.
  const previewCharacterVoice = (c) => {
    const id = `char:${c.id}`;
    // Prefer the cached preview_url saved on the character record.
    // Fallback: stream via /api/voiceover/preview which TTS-es a
    // short sample on demand (slower, costs a tiny TTS call).
    const previewUrl = c.voice_preview_url
      ? c.voice_preview_url
      : `/api/voiceover/preview?voice_id=${encodeURIComponent(c.voice_id)}&text=${encodeURIComponent("Hello — this is " + c.name + ".")}`;
    const track = {
      id,
      isPreview: true,
      segment_label: `${c.name} · voice preview`,
      character_name: c.name,
      version: c.voice_name || "",
      shot_id: null,
      file_path: previewUrl,
      cover_url: c.cover_url || c.image || null,
    };
    setCurrentTrack(track);
    setQueue([]);
    setIsPlaying(true);
    setTimeout(() => {
      if (audioRef.current) {
        try { audioRef.current.currentTime = 0; audioRef.current.play(); } catch (e) {}
      }
    }, 30);
  };

  // Build the audio src for the current track. Previews stream
  // their src directly (ElevenLabs CDN URL or our preview proxy);
  // saved takes go through /api/audio/:id/stream.
  const currentSrc = currentTrack
    ? (currentTrack.isPreview ? currentTrack.file_path : `/api/audio/${currentTrack.id}/stream`)
    : null;

  return (
    <section className="audio-view">
      <header className="audio-head">
        <div>
          <div className="audio-eyebrow">VOICE &amp; VO</div>
          <h1 className="audio-title">Audio</h1>
          <div className="audio-sub">Character voice previews and narration / VO takes.</div>
        </div>
      </header>

      {loading && <div className="audio-empty">Loading…</div>}
      {error   && <div className="audio-empty audio-empty--err">{error}</div>}

      {!loading && !error && (
        <>
          {/* ─── Character voice previews ─────────────────────── */}
          <section className="audio-section">
            <div className="audio-section-head">
              <span className="audio-section-title">CHARACTER VOICES</span>
              <span className="audio-section-count">{charactersWithVoice.length} assigned</span>
            </div>
            {charactersWithVoice.length === 0 && (
              <div className="audio-empty audio-empty--inline">
                No character voices assigned yet. Open a character on the Assets page and pick a voice to see them here.
              </div>
            )}
            {charactersWithVoice.length > 0 && (
              <ul className="audio-list audio-list--chars">
                {charactersWithVoice.map(c => {
                  const isCurrent = currentTrack && currentTrack.id === `char:${c.id}`;
                  return (
                    <CharacterVoiceRow key={c.id}
                      character={c}
                      isCurrent={isCurrent}
                      isPlaying={isPlaying && isCurrent}
                      onPlay={() => {
                        // Pause if already playing this preview.
                        if (isCurrent && audioRef.current) {
                          if (isPlaying) audioRef.current.pause();
                          else audioRef.current.play();
                          return;
                        }
                        previewCharacterVoice(c);
                      }}/>
                  );
                })}
              </ul>
            )}
          </section>

          {/* ─── Narration / VO (grouped) ─────────────────────── */}
          <section className="audio-section">
            <div className="audio-section-head">
              <span className="audio-section-title">NARRATION / VO</span>
              <span className="audio-section-count">{narrationGroups.length} segments · {narrationTakes.length} takes</span>
            </div>
            {narrationGroups.length === 0 && (
              <div className="audio-empty audio-empty--inline">
                No narration yet. Generate a take from the Generate → Voice Over tab and it'll appear here.
              </div>
            )}
            {narrationGroups.map(g => (
              <div key={`${g.segment}::${g.character}`} className="audio-group">
                <div className="audio-group-head">
                  <span className="audio-group-segment">{g.segment}</span>
                  <span className="audio-group-sep">·</span>
                  <span className="audio-group-character">{g.character}</span>
                  <span className="audio-group-count">{g.takes.length} take{g.takes.length === 1 ? "" : "s"}</span>
                </div>
                <ul className="audio-list audio-list--takes">
                  {g.takes.map((a, i) => (
                    <AudioRow
                      key={a.id}
                      asset={a}
                      isCurrent={currentTrack && currentTrack.id === a.id}
                      isPlaying={isPlaying && currentTrack && currentTrack.id === a.id}
                      onPlay={() => togglePlayRow(a)}
                    />
                  ))}
                </ul>
              </div>
            ))}
          </section>
        </>
      )}

      {/* Persistent mini-player. Always rendered while a track is loaded. */}
      {currentTrack && (
        <div className="audio-mini">
          <audio
            ref={audioRef}
            src={currentSrc}
            onPlay={() => setIsPlaying(true)}
            onPause={() => setIsPlaying(false)}
            onEnded={() => { setIsPlaying(false); next(); }}
            onTimeUpdate={(e) => setProgress(e.currentTarget.currentTime || 0)}
            onLoadedMetadata={(e) => setDuration(e.currentTarget.duration || currentTrack.duration_seconds || 0)}
            onError={() => setError(`Could not load ${currentTrack.segment_label || "track"}.`)}
            preload="metadata"
            volume={volume}
          />
          <button type="button" className="audio-mini-prev" onClick={restart} title="Restart"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6 9.5 5.5v-11z"/></svg></button>
          <button type="button" className="audio-mini-play" onClick={togglePlay} title={isPlaying ? "Pause" : "Play"}>
            {isPlaying
              ? <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><rect x="6" y="5" width="4" height="14"/><rect x="14" y="5" width="4" height="14"/></svg>
              : <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M7 5v14l11-7z"/></svg>}
          </button>
          <button type="button" className="audio-mini-next" onClick={next} title="Next" disabled={queue.length === 0}><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M16 6h2v12h-2zm-3.5 6L3 6.5v11z"/></svg></button>
          <div className="audio-mini-info">
            <div className="audio-mini-title">{currentTrack.segment_label || `Track ${currentTrack.id}`}</div>
            <div className="audio-mini-sub">{currentTrack.character_name || "Narration"}{currentTrack.version ? ` · ${currentTrack.version}` : ""}</div>
          </div>
          <div className="audio-mini-time">{fmtAudioTime(progress)}</div>
          <div className="audio-mini-scrub" onClick={(e) => {
            const r = e.currentTarget.getBoundingClientRect();
            seekTo((e.clientX - r.left) / r.width);
          }}>
            <div className="audio-mini-scrub-fill" style={{ width: duration ? `${(progress / duration) * 100}%` : "0%" }}/>
          </div>
          <div className="audio-mini-time">{fmtAudioTime(duration)}</div>
          <input
            className="audio-mini-volume"
            type="range" min={0} max={1} step={0.01}
            value={volume}
            onChange={(e) => {
              const v = Number(e.target.value);
              setVolume(v);
              if (audioRef.current) audioRef.current.volume = v;
            }}
            title="Volume"
          />
        </div>
      )}
    </section>
  );
}

// v06p — Character preview row. Round avatar + name + voice
// label + play/pause toggle. No version / duration metadata —
// previews are a single sample take.
function CharacterVoiceRow({ character, isCurrent, isPlaying, onPlay }) {
  const img = character.cover_url || character.image || null;
  const initials = (character.name || "?").split(" ").slice(0, 2).map(n => n[0] || "").join("").toUpperCase();
  return (
    <li className={"audio-row audio-row--char" + (isCurrent ? " is-current" : "")}>
      <button type="button" className="audio-play-btn" onClick={onPlay} title={isPlaying ? "Pause preview" : "Play preview"}>
        {isPlaying
          ? <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><rect x="6" y="5" width="4" height="14"/><rect x="14" y="5" width="4" height="14"/></svg>
          : <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M7 5v14l11-7z"/></svg>}
      </button>
      <div className="audio-char-avatar"
        /* v07zz31 — Mipmap: 44×44 px avatar, so width-160 = ~3× retina. */
        style={img ? { backgroundImage: `url(${window.thumbUrl ? window.thumbUrl(img, 160) : img})` } : null}>
        {!img && <span className="audio-char-initials">{initials}</span>}
      </div>
      <div className="audio-row-info">
        <div className="audio-row-title">{character.name}</div>
        <div className="audio-row-sub">
          {character.voice_name
            ? <span className="audio-row-version">{character.voice_name}</span>
            : <span className="audio-row-version">voice assigned</span>}
          {character.role && <span className="audio-row-shot">{character.role}</span>}
        </div>
      </div>
    </li>
  );
}

function AudioRow({ asset, isCurrent, isPlaying, onPlay }) {
  // v07w — Hugo: hero toggle + notes per VO take. Hero promotes
  // this take to "the canonical version" of the segment+character
  // group; notes live in /api/notes under entity_type="vo_take",
  // entity_id=String(asset.id). Both are local + persisted +
  // sync via the bidirectional sync.
  const [notesOpen, setNotesOpen] = React.useState(false);
  const [noteCount, setNoteCount] = React.useState(null);
  const [isHero, setIsHero] = React.useState(!!asset.is_hero);
  const fetcher = window.authFetch || fetch;

  // Lightweight count pre-fetch so the button shows "Notes · N"
  // without expanding.
  React.useEffect(() => {
    fetcher(`/api/notes?entity_type=vo_take&entity_id=${encodeURIComponent(asset.id)}&include_resolved=true`)
      .then(r => r.ok ? r.json() : null)
      .then(d => setNoteCount((d && d.notes) ? d.notes.length : 0))
      .catch(() => setNoteCount(0));
  }, [asset.id, fetcher]);

  const toggleHero = (e) => {
    e.stopPropagation();
    const next = !isHero;
    setIsHero(next); // optimistic
    fetcher(`/api/audio/${asset.id}/hero`, {
      method: "PATCH",
      body: JSON.stringify({ is_hero: next }),
    }).catch(() => setIsHero(!next));
  };

  return (
    <li className={"audio-row" + (isCurrent ? " is-current" : "") + (isHero ? " is-hero" : "")}>
      <button type="button" className="audio-play-btn" onClick={onPlay} title={isPlaying ? "Pause" : "Play"}>
        {isPlaying
          ? <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><rect x="6" y="5" width="4" height="14"/><rect x="14" y="5" width="4" height="14"/></svg>
          : <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M7 5v14l11-7z"/></svg>}
      </button>
      <div className="audio-row-info">
        <div className="audio-row-title">
          {asset.version ? <span className="audio-row-version">{asset.version}</span> : null}
          {asset.duration_seconds ? <span className="audio-row-dur">{fmtAudioTime(asset.duration_seconds)}</span> : null}
        </div>
      </div>
      <button
        type="button"
        className={"audio-row-hero" + (isHero ? " is-active" : "")}
        onClick={toggleHero}
        title={isHero ? "Heroed — click to un-hero" : "Mark as hero take"}
      >
        <svg viewBox="0 0 24 24" width="14" height="14" fill={isHero ? "currentColor" : "none"} stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
          <path d="m12 3 2.6 5.4 5.9.8-4.3 4.1 1.1 5.9L12 16.8 6.7 19.2l1.1-5.9L3.5 9.2l5.9-.8z"/>
        </svg>
      </button>
      <button
        type="button"
        className={"audio-row-notes" + (notesOpen ? " is-open" : "")}
        onClick={() => setNotesOpen(o => !o)}
        title={notesOpen ? "Hide notes" : "Show notes"}
      >
        <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
          <path d="M4 4h13l3 3v13H4z"/><path d="M8 9h8M8 13h8M8 17h5"/>
        </svg>
        {noteCount > 0 && <span className="audio-row-notes-count">{noteCount}</span>}
      </button>
      {notesOpen && (
        <VoTakeNotes
          assetId={asset.id}
          onCountChange={setNoteCount}
        />
      )}
    </li>
  );
}

// v07w — Notes section that expands inline below a VO take row.
function VoTakeNotes({ assetId, onCountChange }) {
  const fetcher = window.authFetch || fetch;
  const [notes, setNotes] = React.useState([]);
  const [draft, setDraft] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const load = React.useCallback(() => {
    fetcher(`/api/notes?entity_type=vo_take&entity_id=${encodeURIComponent(assetId)}&include_resolved=true`)
      .then(r => r.ok ? r.json() : null)
      .then(d => {
        const list = (d && d.notes) || [];
        setNotes(list);
        if (onCountChange) onCountChange(list.length);
      })
      .catch(() => {});
  }, [assetId, fetcher, onCountChange]);
  React.useEffect(() => { load(); }, [load]);
  React.useEffect(() => {
    const fn = (e) => {
      try {
        const msg = (e && e.detail) || JSON.parse(e.data || "{}");
        if (msg && (msg.type === "note_added" || msg.type === "note_resolved")) load();
      } catch (_) {}
    };
    window.addEventListener("paradise-sse", fn);
    return () => window.removeEventListener("paradise-sse", fn);
  }, [load]);
  const submit = (e) => {
    e.preventDefault();
    if (!draft.trim() || busy) return;
    setBusy(true);
    fetcher("/api/notes", {
      method: "POST",
      body: JSON.stringify({ entity_type: "vo_take", entity_id: String(assetId), body: draft.trim() }),
    }).then(() => { setDraft(""); load(); }).catch(() => {}).finally(() => setBusy(false));
  };
  return (
    <div className="audio-row-notes-panel">
      {notes.length === 0 ? (
        <div className="audio-row-notes-empty">No notes on this take.</div>
      ) : (
        <ul>
          {notes.map(n => (
            <li key={n.id}>
              <span className="audio-row-notes-body">{n.body}</span>
              <span className="audio-row-notes-meta">— {n.user_name || "—"}</span>
            </li>
          ))}
        </ul>
      )}
      <form onSubmit={submit}>
        <input
          type="text"
          placeholder="Add a note on this take…"
          value={draft}
          onChange={(e) => setDraft(e.target.value)}
          disabled={busy}
        />
        <button type="submit" disabled={!draft.trim() || busy}>Post</button>
      </form>
    </div>
  );
}

Object.assign(window, { AudioPage });
