// Album Hub — the home view. List of songs + album actions.

const ALBUM_HUB_WORKFLOW_STEP_LABELS = {
  upload: "업로드",
  transcribe: "자막 추출",
  scenes: "장면 설계",
  preview: "미리보기",
  export: "내보내기",
  "video-play": "Video Play",
};

function getAlbumHubWorkflowStep(song) {
  if (song?.workflowStep && ALBUM_HUB_WORKFLOW_STEP_LABELS[song.workflowStep]) {
    return song.workflowStep;
  }
  if (song?.status === "ready") return "video-play";
  if (song?.status === "draft") return "scenes";
  return "upload";
}

function getAlbumHubStatusLabel(song) {
  if (song?.status === "ready") return "완성";
  const step = getAlbumHubWorkflowStep(song);
  return `등록중: ${ALBUM_HUB_WORKFLOW_STEP_LABELS[step]}`;
}

function getAlbumHubSongLyrics(song) {
  const timelineLines = [];
  const walk = (node) => {
    if (!node || typeof node !== "object") return;
    if (typeof node.text === "string" && node.text.trim()) {
      timelineLines.push(node.text.trim());
    }
    (node.children || []).forEach(walk);
  };
  if (song?.lyricTimeline?.root) walk(song.lyricTimeline.root);
  const cueLines = (song?.cues || []).map((cue) => String(cue?.text || "").trim()).filter(Boolean);
  return (timelineLines.length ? timelineLines : cueLines).join("\n");
}

function getAlbumHubLyrics(album) {
  return (album?.songs || [])
    .map((song, index) => {
      const lyrics = getAlbumHubSongLyrics(song);
      return [
        `[Song ${index + 1}] ${song?.title || song?.filename || "Untitled"}`,
        lyrics,
      ].filter(Boolean).join("\n");
    })
    .filter(Boolean)
    .join("\n\n");
}

function buildAlbumGoogleImagePrompt(album) {
  return [
    "아래 앨범 전체를 대표하는 정사각형 커버 이미지를 만들어 보자.",
    "앨범의 감정선, 반복되는 시각 모티프, 분위기를 하나의 영화적인 이미지로 표현한다.",
    "텍스트, 자막, 글자, 워터마크, 콜라주, 분할 화면은 넣지 않는다.",
    "<앨범>",
    `제목: ${album?.title || "Untitled"}`,
    `아티스트: ${album?.artist || "Unknown"}`,
    `년도: ${album?.year || ""}`,
    "</앨범>",
    "<전체 노래>",
    getAlbumHubLyrics(album),
    "</전체 노래>",
  ].join("\n");
}

function AlbumHub({
  album,
  setAlbum,
  onOpenSong,
  onNewSong,
  onOpenViewer,
  onBatchRenderVideos,
  onDownloadAlbum,
  onDownloadSongSnapshots,
  onDownloadSongBackup,
  onUploadSongBackup,
  songBackupBusyId = null,
  songBackupUploadBusy = false,
  batchRenderRunning = false,
  albumDownloadBusy = false,
  albumDownloadProgress = 0,
  albumDownloadStage = "",
}) {
  const { Icon, Spectrum, fmtTime } = window;
  const readyCount = album.songs.filter(s => s.status === "ready").length;
  const totalDuration = album.songs.reduce((a, s) => a + (s.duration||0), 0);

  const [editingTitle, setEditingTitle] = useState(false);
  const [titleDraft, setTitleDraft] = useState(album.title);
  const [activeAudioSongId, setActiveAudioSongId] = useState(null);
  const [audioSrc, setAudioSrc] = useState("");
  const [audioPlaying, setAudioPlaying] = useState(false);
  const [loadingAudioSongId, setLoadingAudioSongId] = useState(null);
  const [snapshotBundling, setSnapshotBundling] = useState(false);
  const [audioProgress, setAudioProgress] = useState(0);
  const [audioLevel, setAudioLevel] = useState(0);
  const [audioSpectrum, setAudioSpectrum] = useState([]);
  const [dragSongId, setDragSongId] = useState(null);
  const [dragOverSongId, setDragOverSongId] = useState(null);
  const [dragOverPlacement, setDragOverPlacement] = useState("after");
  const [showAlbumInfoModal, setShowAlbumInfoModal] = useState(false);
  const audioRef = useRef(null);
  const autoplayRef = useRef(false);
  const audioRequestRef = useRef(0);
  const audioMeterFrameRef = useRef(0);
  const audioContextRef = useRef(null);
  const audioAnalyserRef = useRef(null);
  const audioDataRef = useRef(null);
  const audioSourceRef = useRef(null);
  const songBackupInputRef = useRef(null);

  const canPlaySongAudio = (song) => !!(song?.audioServerUrl || song?.audioStorageKey);

  const normalizeAudioUrl = (value) => {
    const raw = String(value || "").trim();
    if (!raw) return "";

    try {
      const url = new URL(raw, window.location.href);
      const current = new URL(window.location.href);
      const isLoopbackHost = (host) => host === "localhost" || host === "127.0.0.1";

      if (isLoopbackHost(url.hostname) && isLoopbackHost(current.hostname) && url.hostname !== current.hostname) {
        url.hostname = current.hostname;
      }

      if (url.origin === current.origin) {
        return `${url.pathname}${url.search}${url.hash}`;
      }

      return url.toString();
    } catch {
      return raw;
    }
  };

  const getNextPlayableSongId = (songId) => {
    const playableSongs = album.songs.filter(canPlaySongAudio);
    if (!playableSongs.length) return null;
    const currentIndex = playableSongs.findIndex((song) => song.id === songId);
    if (currentIndex < 0) return playableSongs[0].id;
    return playableSongs[(currentIndex + 1) % playableSongs.length].id;
  };

  useEffect(() => {
    setTitleDraft(album.title);
  }, [album.id, album.title]);

  const saveTitle = () => {
    setAlbum(prev => ({ ...prev, title: titleDraft.trim() || prev.title }));
    setEditingTitle(false);
  };

  const saveAlbumInfo = (patch) => {
    setAlbum(prev => ({ ...prev, ...patch }));
  };

  const handleDownloadSongSnapshots = async () => {
    if (!album.songs.length || snapshotBundling || typeof onDownloadSongSnapshots !== "function") return;
    setSnapshotBundling(true);
    try {
      await onDownloadSongSnapshots();
    } finally {
      setSnapshotBundling(false);
    }
  };

  const handleDownloadSongBackup = (song, event) => {
    event.stopPropagation();
    if (songBackupBusyId || typeof onDownloadSongBackup !== "function") return;
    onDownloadSongBackup(song.id);
  };

  const handleUploadSongBackup = (event) => {
    const file = event.target.files?.[0];
    event.target.value = "";
    if (!file || songBackupUploadBusy || typeof onUploadSongBackup !== "function") return;
    onUploadSongBackup(file);
  };

  const DownloadGlyph = ({ size = 12 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true" focusable="false">
      <path
        d="M12 3v11m0 0 4-4m-4 4-4-4M5 17.5V20h14v-2.5"
        fill="none"
        stroke="currentColor"
        strokeWidth="2"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );

  const stopAudioMeter = () => {
    if (audioMeterFrameRef.current) {
      cancelAnimationFrame(audioMeterFrameRef.current);
      audioMeterFrameRef.current = 0;
    }
    setAudioLevel(0);
    setAudioSpectrum([]);
  };

  const ensureAudioMeter = async () => {
    const audio = audioRef.current;
    if (!audio) return null;
    const AudioContextCtor = window.AudioContext || window.webkitAudioContext;
    if (!AudioContextCtor) return null;

    if (!audioContextRef.current) {
      const context = new AudioContextCtor();
      const analyser = context.createAnalyser();
      analyser.fftSize = 128;
      analyser.smoothingTimeConstant = 0.82;
      const source = context.createMediaElementSource(audio);
      source.connect(analyser);
      analyser.connect(context.destination);

      audioContextRef.current = context;
      audioAnalyserRef.current = analyser;
      audioSourceRef.current = source;
      audioDataRef.current = new Uint8Array(analyser.frequencyBinCount);
    }

    if (audioContextRef.current.state === "suspended") {
      await audioContextRef.current.resume();
    }

    return audioAnalyserRef.current;
  };

  const startAudioMeter = async () => {
    const analyser = await ensureAudioMeter();
    if (!analyser || !audioDataRef.current) return;

    stopAudioMeter();
    const readLevel = () => {
      const data = audioDataRef.current;
      if (!data || !audioAnalyserRef.current) return;

      audioAnalyserRef.current.getByteFrequencyData(data);
      let sum = 0;
      const spectrumBins = [];
      const targetBins = 40;
      const sliceSize = Math.max(1, Math.floor(data.length / targetBins));

      for (let index = 0; index < data.length; index += 1) {
        sum += data[index];
      }

      for (let binIndex = 0; binIndex < targetBins; binIndex += 1) {
        const start = binIndex * sliceSize;
        const end = Math.min(data.length, start + sliceSize);
        let peak = 0;
        for (let index = start; index < end; index += 1) {
          if (data[index] > peak) peak = data[index];
        }
        const normalized = peak / 255;
        const bandWeight = 1.1 - (binIndex / targetBins) * 0.35;
        spectrumBins.push(Math.min(1, Math.pow(normalized, 0.78) * bandWeight));
      }

      const average = sum / (data.length * 255);
      const nextLevel = Math.min(1, average * 2.6);
      setAudioLevel((prev) => prev * 0.35 + nextLevel * 0.65);
      setAudioSpectrum(spectrumBins);
      audioMeterFrameRef.current = requestAnimationFrame(readLevel);
    };

    audioMeterFrameRef.current = requestAnimationFrame(readLevel);
  };

  useEffect(() => {
    const audio = audioRef.current;
    if (!audio) return;

    const syncProgress = () => {
      const duration = Number(audio.duration || 0);
      const currentTime = Number(audio.currentTime || 0);
      setAudioProgress(duration > 0 ? currentTime / duration : 0);
    };
    const handlePlay = () => {
      setAudioPlaying(true);
      startAudioMeter();
    };
    const handlePause = () => {
      setAudioPlaying(false);
      stopAudioMeter();
    };
    const handleEnded = () => {
      stopAudioMeter();
      setAudioProgress(0);
      const nextSongId = getNextPlayableSongId(activeAudioSongId);
      if (!nextSongId) {
        setAudioPlaying(false);
        return;
      }
      autoplayRef.current = true;
      setActiveAudioSongId(nextSongId);
    };

    audio.addEventListener("play", handlePlay);
    audio.addEventListener("pause", handlePause);
    audio.addEventListener("ended", handleEnded);
    audio.addEventListener("timeupdate", syncProgress);
    audio.addEventListener("loadedmetadata", syncProgress);

    return () => {
      audio.removeEventListener("play", handlePlay);
      audio.removeEventListener("pause", handlePause);
      audio.removeEventListener("ended", handleEnded);
      audio.removeEventListener("timeupdate", syncProgress);
      audio.removeEventListener("loadedmetadata", syncProgress);
    };
  }, [activeAudioSongId, album.songs]);

  useEffect(() => () => {
    stopAudioMeter();
    if (audioContextRef.current && typeof audioContextRef.current.close === "function") {
      audioContextRef.current.close().catch(() => {});
    }
  }, []);

  useEffect(() => {
    if (!activeAudioSongId) return;
    const song = album.songs.find((item) => item.id === activeAudioSongId);
    if (!song || !canPlaySongAudio(song)) {
      const audio = audioRef.current;
      if (audio) {
        audio.pause();
        audio.removeAttribute("src");
        audio.load();
      }
      setActiveAudioSongId(null);
      setAudioSrc("");
      setAudioPlaying(false);
      setAudioProgress(0);
      setLoadingAudioSongId(null);
      stopAudioMeter();
    }
  }, [activeAudioSongId, album.songs]);

  useEffect(() => {
    const song = activeAudioSongId ? album.songs.find((item) => item.id === activeAudioSongId) : null;
    if (!song || !canPlaySongAudio(song)) return;

    const requestId = audioRequestRef.current + 1;
    audioRequestRef.current = requestId;
    setLoadingAudioSongId(song.id);

    window.SongfilmSongStorage.restoreAudioUrl(song)
      .then((restoredUrl) => {
        if (audioRequestRef.current !== requestId) return;
        if (!restoredUrl) throw new Error("mp3를 찾을 수 없습니다.");
        setAudioSrc(restoredUrl);
      })
      .catch((error) => {
        if (audioRequestRef.current !== requestId) return;
        setLoadingAudioSongId(null);
        setActiveAudioSongId(null);
        setAudioSrc("");
        setAudioPlaying(false);
        setAudioProgress(0);
        stopAudioMeter();
        window.toast(error?.message || "mp3를 재생할 수 없습니다.");
      });
  }, [activeAudioSongId, album.songs]);

  useEffect(() => {
    const audio = audioRef.current;
    if (!audio) return;

    if (!audioSrc) {
      audio.pause();
      audio.removeAttribute("src");
      audio.load();
      setLoadingAudioSongId(null);
      setAudioProgress(0);
      stopAudioMeter();
      return;
    }

    audio.crossOrigin = "anonymous";
    audio.src = normalizeAudioUrl(audioSrc);
    audio.load();
    setLoadingAudioSongId(null);

    if (!autoplayRef.current) return;

    autoplayRef.current = false;
    const playPromise = audio.play();
    if (playPromise && playPromise.catch) {
      playPromise.catch(() => {
        setAudioPlaying(false);
        stopAudioMeter();
        window.toast("브라우저가 자동 재생을 막았습니다. 다시 눌러 재생해 주세요.");
      });
    }
  }, [audioSrc]);

  const toggleSongAudio = (song, e) => {
    e.stopPropagation();
    if (!canPlaySongAudio(song)) {
      window.toast("이 노래에는 재생할 mp3가 없습니다.");
      return;
    }

    const audio = audioRef.current;
    const isCurrentSong = activeAudioSongId === song.id;

    if (isCurrentSong && audio && audioSrc) {
      if (audio.paused) {
        const playPromise = audio.play();
        if (playPromise && playPromise.catch) {
          playPromise.catch(() => {
            setAudioPlaying(false);
            stopAudioMeter();
          });
        }
      } else {
        audio.pause();
      }
      return;
    }

    autoplayRef.current = true;
    setActiveAudioSongId(song.id);
  };

  const resetSongDrag = () => {
    setDragSongId(null);
    setDragOverSongId(null);
    setDragOverPlacement("after");
  };

  const handleSongDragStart = (songId, e) => {
    e.stopPropagation();
    setDragSongId(songId);
    setDragOverSongId(songId);
    setDragOverPlacement("after");
    e.dataTransfer.effectAllowed = "move";
    e.dataTransfer.setData("text/plain", songId);
  };

  const handleSongDragOver = (songId, e) => {
    const sourceSongId = dragSongId || e.dataTransfer.getData("text/plain");
    if (!sourceSongId || sourceSongId === songId) return;

    e.preventDefault();
    e.dataTransfer.dropEffect = "move";
    const rect = e.currentTarget.getBoundingClientRect();
    const placement = e.clientY < rect.top + rect.height / 2 ? "before" : "after";
    setDragOverSongId(songId);
    setDragOverPlacement(placement);
  };

  const handleSongDrop = (targetSongId, e) => {
    e.preventDefault();
    e.stopPropagation();

    const sourceSongId = dragSongId || e.dataTransfer.getData("text/plain");
    if (!sourceSongId || sourceSongId === targetSongId) {
      resetSongDrag();
      return;
    }

    const nextSongs = [...album.songs];
    const sourceIndex = nextSongs.findIndex((song) => song.id === sourceSongId);
    const targetIndex = nextSongs.findIndex((song) => song.id === targetSongId);
    if (sourceIndex < 0 || targetIndex < 0) {
      resetSongDrag();
      return;
    }

    const [moved] = nextSongs.splice(sourceIndex, 1);
    const adjustedTargetIndex = sourceIndex < targetIndex ? targetIndex - 1 : targetIndex;
    const insertIndex = dragOverPlacement === "after" ? adjustedTargetIndex + 1 : adjustedTargetIndex;
    nextSongs.splice(insertIndex, 0, moved);
    setAlbum({ ...album, songs: nextSongs });
    resetSongDrag();
  };

  const deleteSong = (id, e) => {
    e.stopPropagation();
    if (activeAudioSongId === id) {
      const audio = audioRef.current;
      if (audio) {
        audio.pause();
        audio.removeAttribute("src");
        audio.load();
      }
      setActiveAudioSongId(null);
      setAudioSrc("");
      setAudioPlaying(false);
      setAudioProgress(0);
      setLoadingAudioSongId(null);
      stopAudioMeter();
    }
    setAlbum({ ...album, songs: album.songs.filter(s => s.id !== id) });
    window.toast("노래를 제거했어요");
  };

  return (
    <div>
      {/* Header */}
      <div className="hub-header">
        <div className="hub-cover">
          <AlbumCover album={album} />
          <div className="cover-title">{album.title}</div>
        </div>
        <div className="hub-meta" style={{flex: 1}}>
          <div className="label">앨범 · {album.year}</div>
          {editingTitle ? (
            <input
              autoFocus
              className="title-edit"
              value={titleDraft}
              onChange={e => setTitleDraft(e.target.value)}
              onBlur={saveTitle}
              onKeyDown={e => { if (e.key === "Enter") saveTitle(); if (e.key === "Escape") setEditingTitle(false); }}
              style={{
                fontFamily:"var(--font-display)", fontWeight:600, fontSize:64,
                background:"transparent", border:"none", outline:"none",
                color:"var(--ink)", width:"100%", padding:0, margin:"6px 0 14px",
                lineHeight: 0.95, letterSpacing:"-0.03em",
              }}
            />
          ) : (
            <div className="title" onClick={() => setEditingTitle(true)} style={{cursor:"text"}}>{album.title}</div>
          )}
          <div className="subtitle">
            <strong>{album.artist}</strong> · {album.songs.length}곡 · {fmtTime(totalDuration)} · 완성 {readyCount}/{album.songs.length}
          </div>
          <div className="hub-actions">
            <button className="pill-btn" onClick={onOpenViewer} disabled={readyCount === 0}>
              <Icon.Play size={14}/> 앨범 재생
            </button>
            <button className="pill-btn" onClick={onNewSong}>
              <Icon.Plus/> 새 노래
            </button>
            <button className="pill-btn" onClick={() => setShowAlbumInfoModal(true)}>
              <Icon.Edit size={11}/> 앨범정보
            </button>
            <button
              className="pill-btn"
              onClick={onBatchRenderVideos}
              disabled={readyCount === 0 || batchRenderRunning}
            >
              <Icon.Film/> {batchRenderRunning ? "배치 렌더링 중…" : "배치 비디오 렌더링"}
            </button>
            <button className="pill-btn" onClick={handleDownloadSongSnapshots} disabled={!album.songs.length || snapshotBundling}>
              <Icon.Download/> {snapshotBundling ? "Song ZIP 준비 중…" : "개발 중 Song ZIP"}
            </button>
            <button className="pill-btn" onClick={onDownloadAlbum} disabled={readyCount === 0 || albumDownloadBusy}>
              <Icon.Download/> {albumDownloadBusy ? `ZIP 준비 중 ${albumDownloadProgress}%` : "앨범 다운로드 (.zip)"}
            </button>
          </div>
          {albumDownloadBusy && (
            <div style={{maxWidth: 420, marginTop: 12}}>
              <div className="progress-track">
                <div className="bar" style={{width: `${albumDownloadProgress}%`}}/>
              </div>
              <div style={{fontSize: 11, color:"var(--ink-3)", fontFamily:"var(--font-mono)", marginTop: 6}}>
                {albumDownloadStage || "앨범 ZIP 준비 중…"}
              </div>
            </div>
          )}
        </div>
      </div>

      {/* Song table */}
      <div className="song-table">
        <div className="song-row-header">
          <div style={{textAlign:"center"}}>#</div>
          <div>제목</div>
          <div>파형</div>
          <div>상태</div>
          <div>장면</div>
          <div style={{textAlign:"right"}}>길이</div>
          <div aria-hidden="true"></div>
        </div>
        {album.songs.map((song, i) => (
          <div
            key={song.id}
            className={[
              "song-row",
              dragSongId === song.id ? "is-dragging" : "",
              dragOverSongId === song.id && dragSongId !== song.id ? `is-drag-over-${dragOverPlacement}` : "",
            ].filter(Boolean).join(" ")}
            onClick={() => {
              if (song.status === "ready") {
                onOpenViewer(song.id);
                return;
              }
              onOpenSong(song.id);
            }}
            onDragOver={(e) => handleSongDragOver(song.id, e)}
            onDrop={(e) => handleSongDrop(song.id, e)}
            onDragLeave={(e) => {
              if (!e.currentTarget.contains(e.relatedTarget)) {
                setDragOverSongId(null);
              }
            }}
            style={{cursor: "pointer"}}
          >
            <div className={`num-cell ${activeAudioSongId === song.id ? "is-audio-active" : ""}`}>
              <span className="num num-value">{i+1}</span>
              {canPlaySongAudio(song) && (
                <button
                  className={`track-play-toggle ${loadingAudioSongId === song.id ? "is-loading" : ""}`}
                  onClick={(e) => toggleSongAudio(song, e)}
                  title={activeAudioSongId === song.id && audioPlaying ? "일시정지" : "재생"}
                  aria-label={activeAudioSongId === song.id && audioPlaying ? "일시정지" : "재생"}
                >
                  {loadingAudioSongId === song.id
                    ? <Icon.Loader size={12}/>
                    : activeAudioSongId === song.id && audioPlaying
                      ? <Icon.Pause size={12}/>
                      : <Icon.Play size={12}/>
                  }
                </button>
              )}
            </div>
            <div>
              <div className="song-title">{song.title}</div>
              <div className="song-sub mono">{song.filename}</div>
            </div>
            <Spectrum
              seed={i + 3}
              bars={40}
              height={28}
              playing={activeAudioSongId === song.id && audioPlaying}
              bins={activeAudioSongId === song.id && audioPlaying ? audioSpectrum : null}
            />
            <div>
              <span className="status-dot">
                <span className={`sd ${song.status}`}/>
                {getAlbumHubStatusLabel(song)}
              </span>
            </div>
            <div>
              <span className="scenes-chip">
                <Icon.Film size={11}/> {song.scenes?.length || 0}
              </span>
            </div>
            <div className="duration">{fmtTime(song.duration || 0)}</div>
            <div className="row-actions">
              <button
                className="icon-btn"
                onClick={(e) => { e.stopPropagation(); onOpenSong(song.id); }}
                title="수정"
                aria-label="수정"
              >
                <Icon.Edit size={12}/>
              </button>
              <button
                className="icon-btn song-backup-btn"
                onClick={(e) => handleDownloadSongBackup(song, e)}
                disabled={!!songBackupBusyId}
                title="Song 전체 백업 다운로드"
                aria-label={`${song.title || "Song"} 전체 백업 다운로드`}
              >
                {songBackupBusyId === song.id ? <Icon.Loader size={12}/> : <DownloadGlyph size={13}/>}
              </button>
              <button
                className="icon-btn"
                onClick={(e) => deleteSong(song.id, e)}
                title="제거"
                aria-label="제거"
              >
                <Icon.X size={12}/>
              </button>
              <button
                className="icon-btn drag-handle"
                draggable
                onClick={(e) => e.stopPropagation()}
                onDragStart={(e) => handleSongDragStart(song.id, e)}
                onDragEnd={resetSongDrag}
                title="드래그해서 순서 변경"
                aria-label="드래그해서 순서 변경"
              >
                <Icon.GripVertical size={13}/>
              </button>
            </div>
          </div>
        ))}
        <div className="new-song-row" onClick={onNewSong}>
          <div style={{textAlign:"center", display:"flex", justifyContent:"center"}}><Icon.Plus size={14}/></div>
          <div>새 노래 추가 — mp3 업로드</div>
        </div>
        <div
          className={`song-backup-upload-row ${songBackupUploadBusy ? "is-busy" : ""}`}
          onClick={() => !songBackupUploadBusy && songBackupInputRef.current?.click()}
        >
          <div style={{textAlign:"center", display:"flex", justifyContent:"center"}}>
            {songBackupUploadBusy ? <Icon.Loader size={14}/> : <Icon.Upload size={14}/>}
          </div>
          <div>{songBackupUploadBusy ? "Song 백업 복원 중…" : "Song 백업 ZIP 업로드 — MP3, MP4, 이미지 포함"}</div>
        </div>
        <input
          ref={songBackupInputRef}
          type="file"
          accept=".zip,application/zip,application/x-zip-compressed"
          hidden
          onChange={handleUploadSongBackup}
        />
      </div>
      {showAlbumInfoModal && (
        <AlbumInfoModal
          album={album}
          onClose={() => setShowAlbumInfoModal(false)}
          onSave={saveAlbumInfo}
        />
      )}
      <audio ref={audioRef} crossOrigin="anonymous" preload="metadata" style={{display:"none"}}/>
    </div>
  );
}

function AlbumInfoModal({ album, onClose, onSave }) {
  const { Icon, SongfilmAIBroker } = window;
  const coverInputRef = useRef();
  const defaultGoogleImagePrompt = buildAlbumGoogleImagePrompt(album);
  const [title, setTitle] = useState(album.title || "");
  const [artist, setArtist] = useState(album.artist || "");
  const [year, setYear] = useState(album.year || new Date().getFullYear());
  const [coverImageUrl, setCoverImageUrl] = useState(album.coverImageUrl || "");
  const [coverImageFilename, setCoverImageFilename] = useState(album.coverImageFilename || "");
  const [coverImagePromptEn, setCoverImagePromptEn] = useState(album.coverImagePromptEn || "");
  const [coverImageProvider, setCoverImageProvider] = useState(album.coverImageProvider === "google" ? "google" : "");
  const [coverGoogleImagePrompt, setCoverGoogleImagePrompt] = useState(album.coverGoogleImagePrompt || defaultGoogleImagePrompt);
  const [coverImageSeed, setCoverImageSeed] = useState(album.coverImageSeed || "");
  const [generatingPrompt, setGeneratingPrompt] = useState(false);
  const [generatingImage, setGeneratingImage] = useState(false);
  const [uploadingCover, setUploadingCover] = useState(false);
  const useGoogleProvider = coverImageProvider === "google";
  const isImageBusy = generatingPrompt || generatingImage || uploadingCover;
  const getDraftAlbum = () => ({ ...album, title, artist, year });
  const previewAlbum = {
    ...album,
    title,
    artist,
    year,
    coverImageUrl,
    coverImageSeed,
  };

  const savePatch = (extra = {}) => {
    const nextTitle = title.trim() || album.title || "Untitled";
    const nextArtist = artist.trim() || "Artist";
    const parsedYear = Number(year);
    onSave({
      title: nextTitle,
      artist: nextArtist,
      year: Number.isFinite(parsedYear) ? parsedYear : year,
      coverImageUrl,
      coverImageFilename,
      coverImagePromptEn,
      coverImageProvider,
      coverGoogleImagePrompt: useGoogleProvider ? coverGoogleImagePrompt : (album.coverGoogleImagePrompt || ""),
      coverImageSeed,
      ...extra,
    });
  };

  const handleGeneratePrompt = async () => {
    if (!SongfilmAIBroker?.generateWholeSongImagePrompt || generatingPrompt) {
      window.toast("Ollama 프롬프트 생성 기능이 로드되지 않았습니다.");
      return;
    }
    setGeneratingPrompt(true);
    try {
      const promptEn = await SongfilmAIBroker.generateWholeSongImagePrompt({
        songTitle: title || album.title,
        artist: artist || album.artist,
        lyrics: getAlbumHubLyrics(getDraftAlbum()),
      });
      setCoverImagePromptEn(promptEn);
      window.toast("앨범 커버 영문 프롬프트를 만들었습니다.");
    } catch (error) {
      console.error("[Album Cover] 프롬프트 생성 실패:", error);
      window.toast(`프롬프트 생성 실패: ${error.message || error}`);
    } finally {
      setGeneratingPrompt(false);
    }
  };

  const handleGenerateImage = async () => {
    if (!SongfilmAIBroker?.generateSceneImage || !SongfilmAIBroker?.saveGeneratedImage || generatingImage) {
      window.toast("이미지 생성 기능이 로드되지 않았습니다.");
      return;
    }
    const promptEn = String(coverImagePromptEn || "").trim();
    const googlePrompt = String(coverGoogleImagePrompt || buildAlbumGoogleImagePrompt(getDraftAlbum())).trim();
    if (!useGoogleProvider && !promptEn) {
      window.toast("먼저 영문 이미지 프롬프트를 생성하거나 입력해 주세요.");
      return;
    }
    setGeneratingImage(true);
    try {
      const seed = Number.isFinite(Number(coverImageSeed)) && coverImageSeed !== ""
        ? Number(coverImageSeed)
        : Math.floor(Math.random() * 1000000);
      const generated = await SongfilmAIBroker.generateSceneImage({
        promptEn: promptEn || googlePrompt,
        seed,
        provider: coverImageProvider,
        promptOverride: useGoogleProvider ? googlePrompt : "",
      });
      const saved = await SongfilmAIBroker.saveGeneratedImage({
        imageBlob: generated.blob,
        storyId: `Album-${album.id || "album"}`,
        chapterNumber: 0,
      });
      SongfilmAIBroker.revokeObjectUrl?.(generated.objectUrl);
      const nextPatch = {
        coverImageUrl: window.SongfilmApiConfig?.getMediaFilename?.(saved.filename || saved.url) || saved.filename || saved.url,
        coverImageFilename: saved.filename,
        coverImagePromptEn: promptEn,
        coverImageProvider,
        coverGoogleImagePrompt: useGoogleProvider ? googlePrompt : (album.coverGoogleImagePrompt || ""),
        coverImageSeed: generated.seed,
        coverImageUpdatedAt: new Date().toISOString(),
      };
      setCoverImageUrl(nextPatch.coverImageUrl);
      setCoverImageFilename(nextPatch.coverImageFilename);
      setCoverImageSeed(nextPatch.coverImageSeed);
      if (useGoogleProvider) setCoverGoogleImagePrompt(googlePrompt);
      savePatch(nextPatch);
      window.toast("앨범 커버 이미지를 생성했습니다.");
    } catch (error) {
      console.error("[Album Cover] 이미지 생성 실패:", error);
      window.toast(`이미지 생성 실패: ${error.message || error}`);
    } finally {
      setGeneratingImage(false);
    }
  };

  const handleToggleGoogleProvider = () => {
    if (useGoogleProvider) {
      setCoverImageProvider("");
      return;
    }
    setCoverImageProvider("google");
    if (!String(coverGoogleImagePrompt || "").trim()) {
      setCoverGoogleImagePrompt(buildAlbumGoogleImagePrompt(getDraftAlbum()));
    }
  };

  const handleCoverUpload = async (event) => {
    const file = Array.from(event.target.files || []).find((item) => item.type?.startsWith("image/"));
    event.target.value = "";
    if (!file || uploadingCover) return;
    if (!SongfilmAIBroker?.saveImageFile) {
      window.toast("이미지 업로드 저장 기능이 로드되지 않았습니다.");
      return;
    }
    setUploadingCover(true);
    try {
      const saved = await SongfilmAIBroker.saveImageFile({
        file,
        storyId: `Album-${album.id || "album"}`,
        chapterNumber: 0,
      });
      const nextPatch = {
        coverImageUrl: window.SongfilmApiConfig?.getMediaFilename?.(saved.filename || saved.url) || saved.filename || saved.url,
        coverImageFilename: saved.filename,
        coverImageUpdatedAt: new Date().toISOString(),
      };
      setCoverImageUrl(nextPatch.coverImageUrl);
      setCoverImageFilename(nextPatch.coverImageFilename);
      savePatch(nextPatch);
      window.toast("앨범 커버 이미지를 업로드했습니다.");
    } catch (error) {
      console.error("[Album Cover] 이미지 업로드 실패:", error);
      window.toast(`이미지 업로드 실패: ${error.message || error}`);
    } finally {
      setUploadingCover(false);
    }
  };

  const handleSave = () => {
    savePatch();
    window.toast("앨범 정보를 저장했습니다.");
    onClose();
  };

  return (
    <div className="modal-overlay" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="scene-detail-modal album-info-modal" onClick={(e) => e.stopPropagation()}>
        <div className="sdm-header">
          <span className="sdm-scene-num">앨범정보</span>
          <span className="sdm-timestamp">{album.songs.length}곡 · Cover Image</span>
          <button className="sdm-close" onClick={onClose}>✕</button>
        </div>

        <div className="sdm-scroll">
          <div className="album-info-cover-preview">
            <AlbumCover album={previewAlbum} />
            <div className="cover-title">{title || album.title}</div>
            {isImageBusy && (
              <div className="sdm-image-loading">
                {generatingPrompt ? "프롬프트 생성 중…" : uploadingCover ? "이미지 업로드 중…" : "이미지 생성 중…"}
              </div>
            )}
          </div>

          <div className="album-info-grid">
            <div>
              <label className="sdm-field-label">앨범 제목</label>
              <input className="album-info-input" value={title} onChange={(e) => setTitle(e.target.value)} />
            </div>
            <div>
              <label className="sdm-field-label">아티스트</label>
              <input className="album-info-input" value={artist} onChange={(e) => setArtist(e.target.value)} />
            </div>
            <div>
              <label className="sdm-field-label">년도</label>
              <input className="album-info-input" value={year} onChange={(e) => setYear(e.target.value)} />
            </div>
          </div>

          <div className="sdm-fields">
            <div>
              <label className="sdm-field-label">
                이미지 생성 프롬프트 (영문)
                <span className="sdm-label-badge">{useGoogleProvider ? "Google 사용 시 참고용" : "기본 이미지 생성"}</span>
              </label>
              <textarea
                className="sdm-textarea mono"
                value={coverImagePromptEn}
                onChange={(e) => setCoverImagePromptEn(e.target.value)}
                rows={5}
                placeholder="Generate with Ollama, then edit the English album-cover prompt."
              />
            </div>

            <div className="album-info-inline-actions">
              <button className="pill-btn" onClick={handleGeneratePrompt} disabled={isImageBusy}>
                <Icon.Sparkle/> {generatingPrompt ? "Ollama 생성 중…" : "Ollama로 영문 프롬프트 만들기"}
              </button>
            </div>

            <div className="sdm-section">
              <label className="sdm-field-label">
                이미지 생성 Provider
                <span className="sdm-label-badge">{useGoogleProvider ? "provider: google" : "기본 local"}</span>
              </label>
              <button
                className={`scene-option-card${useGoogleProvider ? " active" : ""}`}
                onClick={handleToggleGoogleProvider}
                disabled={isImageBusy}
                style={{ width: "100%", textAlign: "left" }}
              >
                <span>Google 이미지 생성 사용</span>
                <small>켜면 아래 한국어 프롬프트를 먼저 확인·수정한 뒤 provider: "google"로 이미지 생성을 호출합니다.</small>
              </button>
            </div>

            {useGoogleProvider && (
              <div>
                <label className="sdm-field-label">
                  Google 이미지 생성 프롬프트
                  <span className="sdm-label-badge">수정 가능</span>
                </label>
                <textarea
                  className="sdm-textarea mono"
                  value={coverGoogleImagePrompt}
                  onChange={(e) => setCoverGoogleImagePrompt(e.target.value)}
                  rows={10}
                />
                <div className="album-info-inline-actions" style={{ marginTop: 8 }}>
                  <button
                    className="pill-btn"
                    onClick={() => setCoverGoogleImagePrompt(buildAlbumGoogleImagePrompt(getDraftAlbum()))}
                    disabled={isImageBusy}
                  >
                    <Icon.Refresh size={11}/> 현재 앨범 정보로 프롬프트 새로고침
                  </button>
                </div>
              </div>
            )}
          </div>
        </div>

        <div className="sdm-actions">
          <input
            ref={coverInputRef}
            type="file"
            accept="image/*"
            style={{ display: "none" }}
            onChange={handleCoverUpload}
          />
          <button className="pill-btn" onClick={() => coverInputRef.current?.click()} disabled={isImageBusy}>
            <Icon.Upload size={11}/> 커버 이미지 업로드
          </button>
          <button className="pill-btn" onClick={onClose}>닫기</button>
          <button className="pill-btn" onClick={handleSave} disabled={isImageBusy}>
            <Icon.Check size={11}/> 저장
          </button>
          <button
            className="pill-btn"
            style={{ background: "var(--accent)", color: "#fff" }}
            onClick={handleGenerateImage}
            disabled={isImageBusy || (!coverImagePromptEn && !useGoogleProvider)}
          >
            {generatingImage ? "생성 중…" : "이미지 생성"}
          </button>
        </div>
      </div>
    </div>
  );
}

// Cover art — procedural collage from scene images
function AlbumCover({ album }) {
  const { SceneImage } = window;
  const coverImageRef = album.coverImageUrl || album.coverImageFilename || "";
  const coverImageUrl = window.SongfilmApiConfig?.buildMediaUrl
    ? window.SongfilmApiConfig.buildMediaUrl("image", coverImageRef)
    : coverImageRef;
  if (coverImageUrl) {
    return (
      <img
        src={coverImageUrl}
        alt={`${album.title || "Album"} cover`}
        style={{position:"absolute", inset:0, width:"100%", height:"100%", objectFit:"cover"}}
      />
    );
  }
  const samples = album.songs
    .flatMap(s => (s.scenes || []).map(sc => ({ sc, paletteName: s.paletteName })))
    .slice(0, 4);
  if (samples.length === 0) return null;
  return (
    <div style={{position:"absolute", inset:0, display:"grid",
      gridTemplateColumns: samples.length >= 4 ? "1fr 1fr" : "1fr",
      gridTemplateRows: samples.length >= 4 ? "1fr 1fr" : "1fr",
    }}>
      {samples.map((s, i) => (
        <div key={i} style={{position:"relative", overflow:"hidden"}}>
          <SceneImage scene={s.sc} paletteName={s.paletteName}/>
        </div>
      ))}
    </div>
  );
}

Object.assign(window, { AlbumHub, AlbumCover });
