import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
    deleteMusicTrack,
    generateMusic,
    generateMusicLyrics,
    getMusicLibrary,
    getMusicProviders,
    getMusicStatus,
    getMusicModels,
    extendMusicTrack,
    removeVocals,
    generateCoverImage,
    convertToWav,
    splitStems,
    getTimestampedLyrics,
    generateStyleBoost,
    generateVideo,
    startBatchGen,
    getBatchJob,
    listGenres,
} from '../../api';
import PullToRefresh from '../../components/PullToRefresh';
import FAB from '../../components/FAB';
import './MusicStudio.css';
import AudioPlayer from './components/AudioPlayer';
import { fmtTime } from './components/AudioPlayer';
import CreditsBadge from './components/CreditsBadge';
import CoverArtModal from './components/CoverArtModal';
import LyricsTab from './components/LyricsTab';
import StemModal from './components/StemModal';
import SyncedLyricsPlayer from './components/SyncedLyricsPlayer';
import RemixTab from './components/RemixTab';
import YoutubeTab from './components/YoutubeTab';
import BatchProgress from './components/BatchProgress';

/* ─────────────────────────────────────────────
   데이터 상수
───────────────────────────────────────────── */
const GENRES = [
    { id: 'lofi',       label: 'Lo-Fi',      icon: '📻', color: '#f5a623', desc: 'Warm · Nostalgic' },
    { id: 'electronic', label: 'Electronic',  icon: '⚡', color: '#60a5fa', desc: 'Pulse · Synth' },
    { id: 'jazz',       label: 'Jazz',        icon: '🎷', color: '#c084fc', desc: 'Smooth · Soul' },
    { id: 'classical',  label: 'Classical',   icon: '🎻', color: '#f9b6b1', desc: 'Orchestral · Grand' },
    { id: 'ambient',    label: 'Ambient',     icon: '🌊', color: '#4aad8b', desc: 'Space · Float' },
    { id: 'hiphop',     label: 'Hip-Hop',     icon: '🎤', color: '#f472b6', desc: 'Beat · Urban' },
    { id: 'rock',       label: 'Rock',        icon: '🎸', color: '#e85c3a', desc: 'Raw · Drive' },
    { id: 'cinematic',  label: 'Cinematic',   icon: '🎬', color: '#fbbf24', desc: 'Epic · Sweeping' },
];

const MOODS = [
    { id: 'energetic',   label: 'Energetic',   color: '#e85c3a' },
    { id: 'chill',       label: 'Chill',        color: '#60a5fa' },
    { id: 'dark',        label: 'Dark',         color: '#9333ea' },
    { id: 'uplifting',   label: 'Uplifting',    color: '#f5a623' },
    { id: 'romantic',    label: 'Romantic',     color: '#f472b6' },
    { id: 'epic',        label: 'Epic',         color: '#fbbf24' },
    { id: 'melancholic', label: 'Melancholic',  color: '#4aad8b' },
];

const INSTRUMENTS = [
    { id: 'piano',   label: 'Piano',   freq: '261Hz' },
    { id: 'guitar',  label: 'Guitar',  freq: '82Hz'  },
    { id: 'drums',   label: 'Drums',   freq: '60Hz'  },
    { id: 'synth',   label: 'Synth',   freq: '440Hz' },
    { id: 'bass',    label: 'Bass',    freq: '41Hz'  },
    { id: 'strings', label: 'Strings', freq: '196Hz' },
    { id: 'brass',   label: 'Brass',   freq: '146Hz' },
    { id: 'flute',   label: 'Flute',   freq: '523Hz' },
    { id: 'violin',  label: 'Violin',  freq: '659Hz' },
    { id: 'choir',   label: 'Choir',   freq: '330Hz' },
];

const DURATIONS = [
    { id: '30s', label: '0:30', sec: 30 },
    { id: '60s', label: '1:00', sec: 60 },
    { id: '90s', label: '1:30', sec: 90 },
    { id: '2m',  label: '2:00', sec: 120 },
    { id: '3m',  label: '3:00', sec: 180 },
    { id: '5m',  label: '5:00', sec: 300 },
];

const BPM_PRESETS = [
    { label: 'Slow', bpm: 70 },
    { label: 'Mid',  bpm: 100 },
    { label: 'Fast', bpm: 130 },
    { label: 'EDM',  bpm: 160 },
];

const KEYS   = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
const SCALES = ['Major','Minor','Dorian','Phrygian','Lydian','Mixolydian'];

/* 시뮬레이션 폴백용 단계 메시지 */
const SIM_STEPS = [
    { msg: 'Analyzing musical patterns…',   pct: 16 },
    { msg: 'Building harmonic structure…',   pct: 32 },
    { msg: 'Rendering instruments…',         pct: 52 },
    { msg: 'Mixing and mastering…',          pct: 72 },
    { msg: 'Applying final polish…',         pct: 90 },
    { msg: 'Track ready!',                   pct: 100 },
];

/* ─────────────────────────────────────────────
   Loading Skeleton
───────────────────────────────────────────── */
const SkeletonCard = () => (
    <div className="ms-lib-card ms-lib-card--skeleton" aria-hidden>
        <div className="ms-lib-card__header">
            <span className="ms-skel ms-skel--icon" />
            <span className="ms-skel ms-skel--title" />
            <span className="ms-skel ms-skel--btn" />
        </div>
        <div className="ms-lib-card__sub">
            <span className="ms-skel ms-skel--filename" />
            <span className="ms-skel ms-skel--meta" />
        </div>
        <div className="ms-lib-card__tags">
            <span className="ms-skel ms-skel--tag" />
            <span className="ms-skel ms-skel--tag" />
        </div>
    </div>
);

/* ─────────────────────────────────────────────
   Waveform Canvas
───────────────────────────────────────────── */
const WaveformCanvas = ({ isGenerating, accentColor }) => {
    const canvasRef    = useRef(null);
    const rafRef       = useRef(null);
    const phaseRef     = useRef(0);
    const intensityRef = useRef(0);

    useEffect(() => {
        const canvas = canvasRef.current;
        if (!canvas) return;
        const ctx = canvas.getContext('2d');
        let dpr = window.devicePixelRatio || 1;

        const resize = () => {
            dpr = window.devicePixelRatio || 1;
            canvas.width  = canvas.offsetWidth  * dpr;
            canvas.height = canvas.offsetHeight * dpr;
            ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        };
        resize();
        const ro = new ResizeObserver(resize);
        ro.observe(canvas);

        const draw = () => {
            const w = canvas.offsetWidth, h = canvas.offsetHeight;
            ctx.clearRect(0, 0, w, h);
            const target = isGenerating ? 1.0 : 0.25;
            intensityRef.current += (target - intensityRef.current) * 0.04;
            const intensity = intensityRef.current;
            const layers = [
                { amp: h * 0.38 * intensity, freq: 1.6, speed: 0.022, alpha: 0.9, lw: 2   },
                { amp: h * 0.22 * intensity, freq: 2.8, speed: 0.034, alpha: 0.5, lw: 1.2 },
                { amp: h * 0.12 * intensity, freq: 4.5, speed: 0.055, alpha: 0.3, lw: 0.8 },
            ];
            layers.forEach(({ amp, freq, speed, alpha, lw }, li) => {
                ctx.beginPath();
                ctx.strokeStyle  = accentColor;
                ctx.globalAlpha  = alpha;
                ctx.lineWidth    = lw;
                ctx.shadowBlur   = isGenerating ? 16 : 6;
                ctx.shadowColor  = accentColor;
                for (let x = 0; x <= w; x += 2) {
                    const t = (x / w) * Math.PI * 2 * freq + phaseRef.current * (1 + li * speed * 10);
                    const h2 = Math.sin(t * 2.1 + li) * 0.35;
                    const h3 = Math.sin(t * 0.45 + li * 0.7) * 0.18;
                    const y  = h / 2 + (Math.sin(t) + h2 + h3) * amp;
                    x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
                }
                ctx.stroke();
            });
            ctx.globalAlpha = 1;
            ctx.shadowBlur  = 0;
            phaseRef.current += isGenerating ? 0.055 : 0.018;
            rafRef.current = requestAnimationFrame(draw);
        };

        draw();
        return () => {
            ro.disconnect();
            cancelAnimationFrame(rafRef.current);
        };
    }, [isGenerating, accentColor]);

    return <canvas ref={canvasRef} className="ms-waveform-canvas" />;
};

/* ─────────────────────────────────────────────
   Sonic Radar (헤더 비주얼 — 모듈 로드 시 1회 계산)
───────────────────────────────────────────── */
const RADAR_N     = 48;
const RADAR_INNER = 26;  // 중심~바 시작 거리(SVG unit)
const RADAR_DATA  = Array.from({ length: RADAR_N }, (_, i) => ({
    angle: `${((i / RADAR_N) * 360).toFixed(2)}deg`,
    delay: `${((i / RADAR_N) * 1.8).toFixed(2)}s`,
    rnd:   `${(0.22 + Math.random() * 0.78).toFixed(2)}`,
}));

const SonicRadar = ({ isGenerating, accentColor }) => (
    <div
        className={`ms-radar ${isGenerating ? 'is-active' : ''}`}
        style={{ '--radar-accent': accentColor }}
        aria-hidden
    >
        {/* SVG — 링·크로스헤어·스윕 */}
        <svg className="ms-radar__svg" viewBox="0 0 160 160">
            {/* 가이드 링 3개 */}
            <circle cx="80" cy="80" r="70" className="ms-radar__ring ms-radar__ring--outer" />
            <circle cx="80" cy="80" r="52" className="ms-radar__ring ms-radar__ring--mid" />
            <circle cx="80" cy="80" r="26" className="ms-radar__ring ms-radar__ring--inner" />

            {/* 크로스헤어 틱 마크 */}
            <line x1="80" y1="6"   x2="80" y2="20"  className="ms-radar__tick" />
            <line x1="80" y1="140" x2="80" y2="154" className="ms-radar__tick" />
            <line x1="6"  y1="80"  x2="20" y2="80"  className="ms-radar__tick" />
            <line x1="140" y1="80" x2="154" y2="80" className="ms-radar__tick" />

            {/* 대각선 코너 틱 */}
            <line x1="19" y1="19" x2="27" y2="27"   className="ms-radar__tick ms-radar__tick--dim" />
            <line x1="141" y1="19" x2="133" y2="27" className="ms-radar__tick ms-radar__tick--dim" />
            <line x1="19" y1="141" x2="27" y2="133" className="ms-radar__tick ms-radar__tick--dim" />
            <line x1="141" y1="141" x2="133" y2="133" className="ms-radar__tick ms-radar__tick--dim" />

            {/* 레이더 스윕 라인 (generating 시 회전) */}
            <line x1="80" y1="80" x2="80" y2="10" className="ms-radar__sweep" />

            {/* 센터 글로우 링 */}
            <circle cx="80" cy="80" r="14" className="ms-radar__center-ring" />
        </svg>

        {/* CSS 방사형 바 (HTML div) */}
        {RADAR_DATA.map((bar, i) => (
            <div
                key={i}
                className="ms-radar__pivot"
                style={{ '--angle': bar.angle }}
            >
                <div
                    className="ms-radar__bar"
                    style={{ '--delay': bar.delay, '--rnd': bar.rnd }}
                />
            </div>
        ))}

        {/* 센터 도트 */}
        <div className="ms-radar__center" />

        {/* 상태 레이블 */}
        <div className="ms-radar__status">
            <span className="ms-radar__status-dot" />
            {isGenerating ? 'GENERATING' : 'STANDBY'}
        </div>
    </div>
);

/* ─────────────────────────────────────────────
   Progress Bar
───────────────────────────────────────────── */
const GenerationProgress = ({ progress, stepMsg }) => (
    <div className="ms-progress">
        <div className="ms-progress__bar">
            <div className="ms-progress__fill" style={{ width: `${progress}%` }} />
        </div>
        <div className="ms-progress__meta">
            <span className="ms-progress__msg">{stepMsg}</span>
            <span className="ms-progress__pct">{progress}%</span>
        </div>
    </div>
);

/* ─────────────────────────────────────────────
   Track Result Card
───────────────────────────────────────────── */
const TrackResult = ({ track, onDownload, onNew }) => {
    const genre = GENRES.find((g) => g.id === track.genre);
    const totalSec = DURATIONS.find((d) => d.id === track.duration)?.sec ?? track.duration_sec ?? 60;

    return (
        <div className="ms-result" style={{ '--result-accent': genre?.color }}>
            <div className="ms-result__header">
                <span className="ms-result__badge">✓ Generated</span>
                <span className="ms-result__time">{track.created_at ?? track.createdAt}</span>
            </div>

            <div className="ms-result__title-row">
                <span className="ms-result__icon">{genre?.icon}</span>
                <div>
                    <h3 className="ms-result__title">{track.title}</h3>
                    <p className="ms-result__meta">
                        {fmtTime(totalSec)} · {track.bpm} BPM · {track.key} {track.scale}
                    </p>
                </div>
            </div>

            <AudioPlayer
                audioUrl={track.audio_url}
                totalSec={totalSec}
                accentColor={genre?.color ?? '#f5a623'}
            />

            <div className="ms-result__tags">
                {track.provider && (
                    <span className={`ms-result__tag ms-result__tag--provider ${track.provider === 'suno' ? 'is-suno' : 'is-local'}`}>
                        {track.provider === 'suno' ? '🎙️ Suno' : '🤖 MusicGen'}
                    </span>
                )}
                {(track.instruments ?? []).slice(0, 4).map((inst) => (
                    <span key={inst} className="ms-result__tag">{inst}</span>
                ))}
                {track.moods?.map?.((m) => (
                    <span key={m} className="ms-result__tag">{m}</span>
                ))}
            </div>

            {track.lyrics && (
                <details className="ms-result__lyrics">
                    <summary>🎤 가사 보기</summary>
                    <pre className="ms-result__lyrics-text">{track.lyrics}</pre>
                </details>
            )}

            <div className="ms-result__actions">
                <button type="button" className="ms-btn ms-btn--ghost" onClick={onNew}>
                    + New Track
                </button>
                {track.audio_url && (
                    <a href={track.audio_url} download className="ms-btn ms-btn--outline">
                        ↓ Download
                    </a>
                )}
            </div>
            <p className="ms-result__yt-hint">
                ✓ 생성 완료 — Library에 자동 저장되었습니다
            </p>
        </div>
    );
};

/* ─────────────────────────────────────────────
   Library Card
───────────────────────────────────────────── */
const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, onVideoGenerate, onVideoProject, onVideoPipeline, isGenerating }) => {
    const [menuOpen, setMenuOpen] = useState(false);
    const genre    = GENRES.find((g) => g.id === track.genre);
    const totalSec = track.duration_sec ?? null;
    const filename = track.audio_url ? track.audio_url.split('/').pop() : '';
    const hasSunoId = !!track.suno_id;

    return (
        <div
            className={`ms-lib-card ${isPlaying ? 'is-playing' : ''}`}
            style={{ '--lib-accent': genre?.color ?? '#f5a623' }}
        >
            <div className="ms-lib-card__header">
                <span className="ms-lib-card__icon">{genre?.icon ?? '🎵'}</span>
                <p className="ms-lib-card__title">{track.title}</p>
                <div className="ms-lib-card__controls">
                    <button
                        type="button"
                        className={`ms-btn--icon ${isPlaying ? 'is-active' : ''}`}
                        onClick={() => onPlay(track)}
                        aria-label={isPlaying ? '정지' : '재생'}
                    >
                        {isPlaying ? '■' : '▶'}
                    </button>
                    <button
                        type="button"
                        className="ms-btn--icon ms-btn--danger"
                        onClick={() => onDelete(track.id)}
                        aria-label="삭제"
                    >
                        ✕
                    </button>
                </div>
            </div>
            <div className="ms-lib-card__sub">
                <p className="ms-lib-card__filename">{filename}</p>
                <p className="ms-lib-card__meta">
                    {totalSec != null ? fmtTime(totalSec) : '--:--'} · {track.bpm ? `${track.bpm} BPM` : ''} {track.key} {track.scale}
                </p>
            </div>
            {isPlaying && (
                <AudioPlayer
                    audioUrl={track.audio_url}
                    totalSec={totalSec}
                    accentColor={genre?.color ?? '#f5a623'}
                />
            )}
            <div className="ms-lib-card__tags">
                {track.provider && (
                    <span className={`ms-result__tag ms-result__tag--provider ${track.provider === 'suno' ? 'is-suno' : 'is-local'}`}>
                        {track.provider === 'suno' ? '🎙️ Suno' : '🤖 MusicGen'}
                    </span>
                )}
                {(track.instruments ?? []).slice(0, 3).map((i) => (
                    <span key={i} className="ms-result__tag">{i}</span>
                ))}
                {(track.moods ?? []).slice(0, 2).map((m) => (
                    <span key={m} className="ms-result__tag">{m}</span>
                ))}
            </div>
            {hasSunoId && (
                <div className="ms-lib-card__actions">
                    <button type="button" className="ms-btn ms-btn--ghost ms-btn--sm"
                        onClick={() => onExtend(track)} disabled={isGenerating}>
                        ⏩ Extend
                    </button>
                    <button type="button" className="ms-btn ms-btn--ghost ms-btn--sm"
                        onClick={() => onVocalRemoval(track)} disabled={isGenerating}>
                        🎤 Vocal Split
                    </button>
                    {track.audio_url && (
                        <a href={track.audio_url} download className="ms-btn ms-btn--ghost ms-btn--sm">↓ Download</a>
                    )}
                    <div className="ms-more-menu">
                        <button type="button" className="ms-btn ms-btn--ghost ms-btn--sm"
                            onClick={() => setMenuOpen(!menuOpen)}>•••</button>
                        {menuOpen && (
                            <div className="ms-more-menu__dropdown">
                                <button type="button" onClick={() => { onCoverArt(track); setMenuOpen(false); }}
                                    disabled={isGenerating}>🖼 Cover Art</button>
                                <button type="button" onClick={() => { onWavConvert(track); setMenuOpen(false); }}
                                    disabled={isGenerating}>📀 WAV Download</button>
                                <button type="button" onClick={() => { onStemSplit(track); setMenuOpen(false); }}
                                    disabled={isGenerating}>🎛 12 Stems (50cr)</button>
                                <button type="button" onClick={() => { onSyncedLyrics(track); setMenuOpen(false); }}
                                    disabled={isGenerating || !track.lyrics}>📝 Synced Lyrics</button>
                                <button type="button" onClick={() => { onVideoGenerate(track); setMenuOpen(false); }}
                                    disabled={isGenerating}>🎬 Music Video</button>
                                <button type="button" onClick={() => { onVideoProject(track); setMenuOpen(false); }}>
                                    🎯 YouTube 프로젝트
                                </button>
                                <button type="button" onClick={() => { onVideoPipeline(track); setMenuOpen(false); }}>
                                    🎬 영상 파이프라인
                                </button>
                            </div>
                        )}
                    </div>
                </div>
            )}
            {!hasSunoId && track.audio_url && (
                <div className="ms-lib-card__actions">
                    <a href={track.audio_url} download className="ms-btn ms-btn--ghost ms-btn--sm">
                        ↓ Download
                    </a>
                    <button type="button" className="ms-btn ms-btn--ghost ms-btn--sm"
                        onClick={() => onVideoPipeline(track)}>
                        🎬 영상 파이프라인
                    </button>
                </div>
            )}
            <p className="ms-lib-card__date">
                {track.created_at ? new Date(track.created_at).toLocaleDateString('ko-KR') : ''}
            </p>
        </div>
    );
};

/* ─────────────────────────────────────────────
   Library Section
───────────────────────────────────────────── */
const Library = ({ tracks, onDelete, onRefresh, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, onVideoGenerate, onVideoProject, onVideoPipeline, isGenerating, loading }) => {
    const [playingId, setPlayingId] = useState(null);

    const handlePlay = (track) => {
        setPlayingId((prev) => (prev === track.id ? null : track.id));
    };

    if (loading) {
        return (
            <div className="ms-library">
                <div className="ms-library__header">
                    <h2 className="ms-library__title">My Library</h2>
                </div>
                <div className="ms-library__grid">
                    {Array.from({ length: 4 }, (_, i) => <SkeletonCard key={i} />)}
                </div>
            </div>
        );
    }

    if (tracks.length === 0) {
        return (
            <div className="ms-library ms-library--empty">
                <div className="ms-library__empty-icon">🎵</div>
                <p className="ms-library__empty-text">라이브러리가 비어있습니다</p>
                <p className="ms-library__empty-hint">트랙을 생성하고 Library에 저장하면 여기서 확인할 수 있습니다</p>
            </div>
        );
    }

    return (
        <div className="ms-library">
            <div className="ms-library__header">
                <h2 className="ms-library__title">My Library</h2>
                <span className="ms-library__count">{tracks.length} tracks</span>
                <button type="button" className="ms-btn ms-btn--ghost ms-library__refresh" onClick={onRefresh}>
                    ↻ Refresh
                </button>
            </div>
            <div className="ms-library__grid">
                {tracks.map((track) => (
                    <LibraryCard
                        key={track.id}
                        track={track}
                        onDelete={onDelete}
                        onPlay={handlePlay}
                        isPlaying={playingId === track.id}
                        onExtend={onExtend}
                        onVocalRemoval={onVocalRemoval}
                        onCoverArt={onCoverArt}
                        onWavConvert={onWavConvert}
                        onStemSplit={onStemSplit}
                        onSyncedLyrics={onSyncedLyrics}
                        onVideoGenerate={onVideoGenerate}
                        onVideoProject={onVideoProject}
                        onVideoPipeline={onVideoPipeline}
                        isGenerating={isGenerating}
                    />
                ))}
            </div>
        </div>
    );
};

/* ─────────────────────────────────────────────
   Main Page
───────────────────────────────────────────── */
export default function MusicStudio() {
    /* ── 탭 ── */
    const [tab, setTab] = useState('create');
    const [initialTrackId, setInitialTrackId] = useState(null);
    const [openPipelineFor, setOpenPipelineFor] = useState(null);

    /* ── Provider 상태 ── */
    const [providers,    setProviders]    = useState([]);
    const [provider,     setProvider]     = useState('suno');
    const [providerError, setProviderError] = useState(false);

    /* ── 컨트롤 상태 ── */
    const [genre,      setGenre]      = useState(null);
    const [moods,      setMoods]      = useState([]);
    const [instruments, setInstruments] = useState([]);
    const [duration,   setDuration]   = useState('60s');
    const [bpm,        setBpm]        = useState(100);
    const [musicalKey, setMusicalKey] = useState('C');
    const [scale,      setScale]      = useState('Major');
    const [prompt,     setPrompt]     = useState('');
    const [customTitle, setCustomTitle] = useState('');

    /* ── Suno 전용 상태 ── */
    const [lyrics,        setLyrics]        = useState('');
    const [instrumental,  setInstrumental]  = useState(false);
    const [lyricsLoading, setLyricsLoading] = useState(false);
    const [model,         setModel]         = useState('V4');
    const [models,        setModels]        = useState([]);

    /* ── Phase 1: 신규 파라미터 ── */
    const [vocalGender, setVocalGender]     = useState(null);       // "m" | "f" | null
    const [negativeTags, setNegativeTags]   = useState('');
    const [styleWeight, setStyleWeight]     = useState(50);         // UI: 0~100
    const [audioWeight, setAudioWeight]     = useState(50);

    /* ── CoverArt 상태 ── */
    const [coverArtModal, setCoverArtModal] = useState(null); // { trackId, images }

    /* ── Phase 2 상태 ── */
    const [stemModal, setStemModal] = useState(null);         // { stems: {} }
    const [syncedLyrics, setSyncedLyrics] = useState(null);   // { audioUrl, words }
    const [styleBoostLoading, setStyleBoostLoading] = useState(false);

    /* ── 생성 상태 ── */
    const [isGenerating, setIsGenerating] = useState(false);
    const [genProgress,  setGenProgress]  = useState(0);
    const [genStep,      setGenStep]      = useState('');
    const [genError,     setGenError]     = useState(null);
    const [track,        setTrack]        = useState(null);

    /* ── 라이브러리 상태 ── */
    const [library,     setLibrary]     = useState([]);
    const [libLoading,  setLibLoading]  = useState(false);

    /* ── 설명 토글 ── */
    const [openDescs, setOpenDescs] = useState(new Set());
    const toggleDesc = (id) => setOpenDescs((prev) => {
        const next = new Set(prev);
        next.has(id) ? next.delete(id) : next.add(id);
        return next;
    });
    const isOpen = (id) => openDescs.has(id);

    /* ── refs ── */
    const pollRef   = useRef(null);
    const taskIdRef = useRef(null);

    /* ── 배치 생성 상태 ── */
    const [batchOpen, setBatchOpen] = useState(false);
    const [batchGenre, setBatchGenre] = useState('lo-fi');
    const [batchCount, setBatchCount] = useState(10);
    const [batchDuration, setBatchDuration] = useState(180);
    const [batchAutoPipe, setBatchAutoPipe] = useState(true);
    const [currentBatch, setCurrentBatch] = useState(null);
    const [batchPolling, setBatchPolling] = useState(false);
    const [batchGenresList, setBatchGenresList] = useState(['lo-fi', 'phonk', 'ambient', 'pop']);
    const batchPollRef = useRef(null);

    const activeGenre = GENRES.find((g) => g.id === genre);
    const accentColor = activeGenre?.color ?? '#f5a623';

    /* ── Provider 로드 ── */
    useEffect(() => {
        getMusicProviders()
            .then((data) => {
                const list = data.providers ?? [];
                setProviders(list);
                setProviderError(false);
                if (list.length > 0 && !list.find((p) => p.id === provider)) {
                    setProvider(list[0].id);
                }
            })
            .catch(() => setProviderError(true));
    }, []); // eslint-disable-line react-hooks/exhaustive-deps

    /* ── 모델 로드 ── */
    useEffect(() => {
        getMusicModels()
            .then((data) => setModels(data.models ?? []))
            .catch(() => {});
    }, []);

    /* ── 가사 AI 생성 ── */
    const handleGenerateLyrics = async () => {
        if (!prompt && !genre) return;
        setLyricsLoading(true);
        try {
            const desc = prompt || `${GENRES.find((g) => g.id === genre)?.label ?? ''} ${moods.map((id) => MOODS.find((m) => m.id === id)?.label).join(' ')}`;
            const result = await generateMusicLyrics(desc);
            if (result?.text) setLyrics(result.text);
        } catch {}
        finally { setLyricsLoading(false); }
    };

    /* ── 라이브러리 로드 ── */
    const loadLibrary = useCallback(async () => {
        setLibLoading(true);
        try {
            const data = await getMusicLibrary();
            setLibrary(data.tracks ?? []);
        } catch {
            /* 백엔드 미구현 시 무시 */
        } finally {
            setLibLoading(false);
        }
    }, []);

    useEffect(() => { loadLibrary(); }, [loadLibrary]);

    /* ── 탭 전환 시 라이브러리 갱신 ── */
    useEffect(() => {
        if (tab === 'library') loadLibrary();
    }, [tab, loadLibrary]);

    /* ── 언마운트 시 폴링 정리 ── */
    useEffect(() => () => clearInterval(pollRef.current), []);

    /* ── 배치 생성 시작 ── */
    const startBatch = async () => {
        try {
            const res = await startBatchGen({
                genre: batchGenre,
                count: batchCount,
                target_duration_sec: batchDuration,
                auto_pipeline: batchAutoPipe,
            });
            setCurrentBatch(res);
            setBatchPolling(true);
        } catch (e) {
            alert(`배치 시작 실패: ${e.message || e}`);
        }
    };

    /* ── 배치: 지원 장르 목록 fetch (mount 시 1회) ── */
    useEffect(() => {
        listGenres()
            .then((r) => {
                if (Array.isArray(r?.genres) && r.genres.length) {
                    setBatchGenresList(r.genres);
                    if (!r.genres.includes(batchGenre)) setBatchGenre(r.genres[0]);
                }
            })
            .catch(() => { /* fallback hardcoded list 유지 */ });
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    /* ── 배치 폴링 ── */
    useEffect(() => {
        if (!batchPolling || !currentBatch?.id) return;
        const tick = async () => {
            try {
                const j = await getBatchJob(currentBatch.id);
                if (j) {
                    setCurrentBatch(j);
                    if (['piped', 'failed', 'cancelled'].includes(j.status)) {
                        setBatchPolling(false);
                        // library 갱신 (새 트랙들 표시되도록)
                        if (typeof loadLibrary === 'function') loadLibrary();
                    }
                }
            } catch { /* swallow */ }
        };
        batchPollRef.current = setInterval(tick, 5000);
        return () => clearInterval(batchPollRef.current);
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [batchPolling, currentBatch?.id]);

    /* ── helpers ── */
    const toggleMood = (id) =>
        setMoods((prev) =>
            prev.includes(id) ? prev.filter((m) => m !== id)
                : prev.length < 3 ? [...prev, id] : prev
        );

    const toggleInstrument = (id) =>
        setInstruments((prev) =>
            prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]
        );

    /* ── 시뮬레이션 폴백 ── */
    const runSimulation = async (fallbackData) => {
        for (const step of SIM_STEPS) {
            setGenStep(step.msg);
            await new Promise((r) => setTimeout(r, 700 + Math.random() * 500));
            setGenProgress(step.pct);
        }
        setIsGenerating(false);
        const durSec = DURATIONS.find((d) => d.id === duration)?.sec ?? 60;
        setTrack({
            id:          `sim_${Date.now()}`,
            title:       `${activeGenre?.label} — ${moods[0] ? MOODS.find((m) => m.id === moods[0])?.label : 'Original'} Mix`,
            genre,
            duration,
            duration_sec: durSec,
            bpm,
            key:          musicalKey,
            scale,
            moods,
            instruments:  instruments.length > 0 ? instruments : ['piano', 'synth', 'bass'],
            audio_url:    null,
            created_at:   new Date().toLocaleTimeString('ko-KR'),
            ...fallbackData,
        });
    };

    /* ── 폴링 ── */
    const startPolling = (taskId, trackTitle) => {
        clearInterval(pollRef.current);
        pollRef.current = setInterval(async () => {
            try {
                const status = await getMusicStatus(taskId);
                setGenProgress(status.progress ?? 0);
                setGenStep(status.message ?? '처리 중…');

                if (status.status === 'succeeded') {
                    clearInterval(pollRef.current);
                    setIsGenerating(false);
                    setGenProgress(100);
                    /* status.track이 있으면 library와 동일한 객체 사용, 없으면 로컬 state로 조립 */
                    if (status.track) {
                        setTrack(status.track);
                    } else {
                        const durSec = DURATIONS.find((d) => d.id === duration)?.sec ?? 60;
                        setTrack({
                            id:           taskId,
                            title:        trackTitle,
                            genre,
                            duration_sec: durSec,
                            bpm,
                            key:          musicalKey,
                            scale,
                            moods,
                            instruments:  instruments.length > 0 ? instruments : ['piano', 'synth', 'bass'],
                            audio_url:    status.audio_url ?? null,
                            created_at:   new Date().toLocaleTimeString('ko-KR'),
                        });
                    }
                    /* 백엔드 auto-register 후 Library 자동 갱신 */
                    loadLibrary();
                } else if (status.status === 'failed') {
                    clearInterval(pollRef.current);
                    setIsGenerating(false);
                    setGenError(`생성 실패: ${status.error ?? '알 수 없는 오류'}`);
                }
            } catch {
                clearInterval(pollRef.current);
                runSimulation({});
            }
        }, 3000);
    };

    /* ── 생성 핸들러 ── */
    const handleGenerate = async () => {
        if (!genre || isGenerating) return;
        setIsGenerating(true);
        setTrack(null);
        setGenProgress(0);
        setGenStep('요청 전송 중…');
        setGenError(null);

        const durSec   = DURATIONS.find((d) => d.id === duration)?.sec ?? 60;
        const moodLabel = moods[0] ? MOODS.find((m) => m.id === moods[0])?.label : 'Original';
        const title    = customTitle.trim() || `${activeGenre?.label} — ${moodLabel} Mix`;
        const instList = instruments.length > 0 ? instruments : ['piano', 'synth', 'bass'];

        const payload = {
            provider,
            model,
            title,
            genre,
            moods,
            instruments: instList,
            duration_sec: durSec,
            bpm,
            key:   musicalKey,
            scale,
            prompt: prompt || undefined,
            ...(provider === 'suno' ? {
                lyrics: lyrics || undefined,
                instrumental,
                vocal_gender: vocalGender || undefined,
                negative_tags: negativeTags || undefined,
                style_weight: styleWeight !== 50 ? styleWeight / 100 : undefined,
                audio_weight: audioWeight !== 50 ? audioWeight / 100 : undefined,
            } : {}),
        };

        try {
            const res = await generateMusic(payload);
            if (res?.task_id) {
                taskIdRef.current = res.task_id;
                setGenStep('AI가 음악을 작곡하고 있습니다…');
                setGenProgress(5);
                startPolling(res.task_id, title);
            } else {
                /* task_id 없이 바로 결과 반환하는 경우 */
                setIsGenerating(false);
                setGenProgress(100);
                setTrack({ ...payload, id: Date.now(), audio_url: res.audio_url ?? null, created_at: new Date().toLocaleTimeString('ko-KR') });
                loadLibrary();
            }
        } catch {
            /* API 없을 때 시뮬레이션 폴백 */
            setGenStep('오프라인 모드: 시뮬레이션 진행 중…');
            await runSimulation({ title });
        }
    };

    /* ── 라이브러리 삭제 ── */
    const handleDeleteFromLibrary = async (id) => {
        try {
            await deleteMusicTrack(id);
        } catch { /* 무시 */ }
        setLibrary((prev) => prev.filter((t) => t.id !== id));
    };

    /* ── 곡 연장 핸들러 ── */
    const handleExtend = async (track) => {
        if (!track.suno_id || isGenerating) return;
        setTab('create');
        setIsGenerating(true);
        setTrack(null);
        setGenProgress(0);
        setGenStep('곡 연장 요청 중…');
        setGenError(null);
        try {
            const res = await extendMusicTrack({
                suno_id: track.suno_id,
                continue_at: track.duration_sec ?? 60,
                prompt: '',
                style: track.genre ?? '',
                title: `${track.title} (Extended)`,
                model,
            });
            if (res?.task_id) {
                taskIdRef.current = res.task_id;
                setGenStep('AI가 곡을 연장하고 있습니다…');
                setGenProgress(5);
                startPolling(res.task_id, `${track.title} (Extended)`);
            }
        } catch {
            setIsGenerating(false);
            setGenError('곡 연장에 실패했습니다');
        }
    };

    /* ── 보컬 분리 핸들러 ── */
    const handleVocalRemoval = async (track) => {
        if (!track.suno_id || isGenerating) return;
        setTab('create');
        setIsGenerating(true);
        setTrack(null);
        setGenProgress(0);
        setGenStep('보컬 분리 요청 중…');
        setGenError(null);
        try {
            const res = await removeVocals({
                suno_id: track.suno_id,
                title: track.title,
            });
            if (res?.task_id) {
                taskIdRef.current = res.task_id;
                setGenStep('AI가 보컬을 분리하고 있습니다…');
                setGenProgress(5);
                startPolling(res.task_id, `${track.title} (Vocal Removed)`);
            }
        } catch {
            setIsGenerating(false);
            setGenError('보컬 분리에 실패했습니다');
        }
    };

    /* ── 커버 아트 핸들러 ── */
    const handleCoverArt = async (track) => {
        if (!track.task_id || isGenerating) return;
        setTab('create');
        setIsGenerating(true);
        setTrack(null);
        setGenProgress(0);
        setGenStep('커버 이미지 생성 요청 중…');
        setGenError(null);
        try {
            const res = await generateCoverImage({
                suno_task_id: track.task_id,
                track_id: track.id,
            });
            if (res?.task_id) {
                taskIdRef.current = res.task_id;
                setGenStep('AI가 커버 이미지를 생성하고 있습니다…');
                setGenProgress(5);
                clearInterval(pollRef.current);
                pollRef.current = setInterval(async () => {
                    try {
                        const status = await getMusicStatus(res.task_id);
                        setGenProgress(status.progress ?? 0);
                        setGenStep(status.message ?? '처리 중…');
                        if (status.status === 'succeeded') {
                            clearInterval(pollRef.current);
                            setIsGenerating(false);
                            const images = JSON.parse(status.audio_url || '[]');
                            setCoverArtModal({ trackId: track.id, images });
                        } else if (status.status === 'failed') {
                            clearInterval(pollRef.current);
                            setIsGenerating(false);
                            setGenError(`커버 이미지 생성 실패: ${status.error ?? '알 수 없는 오류'}`);
                        }
                    } catch {
                        clearInterval(pollRef.current);
                        setIsGenerating(false);
                        setGenError('커버 이미지 상태 조회 실패');
                    }
                }, 3000);
            }
        } catch {
            setIsGenerating(false);
            setGenError('커버 이미지 생성에 실패했습니다');
        }
    };

    const handleCoverSelect = (imageUrl) => {
        if (coverArtModal?.trackId) {
            setLibrary((prev) => prev.map((t) =>
                t.id === coverArtModal.trackId
                    ? { ...t, cover_images: [imageUrl, ...(coverArtModal.images || []).filter(u => u !== imageUrl)] }
                    : t
            ));
        }
        setCoverArtModal(null);
    };

    /* ── WAV 변환 핸들러 ── */
    const handleWavConvert = async (track) => {
        if (!track.task_id || !track.suno_id || isGenerating) return;
        setTab('create');
        setIsGenerating(true);
        setTrack(null);
        setGenProgress(0);
        setGenStep('WAV 변환 요청 중…');
        setGenError(null);
        try {
            const res = await convertToWav({
                suno_task_id: track.task_id,
                suno_id: track.suno_id,
                track_id: track.id,
            });
            if (res?.task_id) {
                taskIdRef.current = res.task_id;
                setGenStep('WAV 변환 처리 중…');
                setGenProgress(5);
                clearInterval(pollRef.current);
                pollRef.current = setInterval(async () => {
                    try {
                        const status = await getMusicStatus(res.task_id);
                        setGenProgress(status.progress ?? 0);
                        setGenStep(status.message ?? '처리 중…');
                        if (status.status === 'succeeded') {
                            clearInterval(pollRef.current);
                            setIsGenerating(false);
                            const wavUrl = status.audio_url;
                            if (wavUrl) {
                                const a = document.createElement('a');
                                a.href = wavUrl;
                                a.download = `${track.title || 'track'}.wav`;
                                a.click();
                            }
                            setGenStep('WAV 변환 완료!');
                        } else if (status.status === 'failed') {
                            clearInterval(pollRef.current);
                            setIsGenerating(false);
                            setGenError(`WAV 변환 실패: ${status.error ?? '알 수 없는 오류'}`);
                        }
                    } catch {
                        clearInterval(pollRef.current);
                        setIsGenerating(false);
                        setGenError('WAV 변환 상태 조회 실패');
                    }
                }, 3000);
            }
        } catch {
            setIsGenerating(false);
            setGenError('WAV 변환에 실패했습니다');
        }
    };

    /* ── 12스템 분리 핸들러 ── */
    const handleStemSplit = async (track) => {
        if (!track.task_id || !track.suno_id || isGenerating) return;
        setTab('create');
        setIsGenerating(true);
        setTrack(null);
        setGenProgress(0);
        setGenStep('12스템 분리 요청 중…');
        setGenError(null);
        try {
            const res = await splitStems({
                suno_task_id: track.task_id,
                suno_id: track.suno_id,
                track_id: track.id,
            });
            if (res?.task_id) {
                taskIdRef.current = res.task_id;
                setGenStep('12스템 분리 처리 중 (약 2~3분)…');
                setGenProgress(5);
                clearInterval(pollRef.current);
                pollRef.current = setInterval(async () => {
                    try {
                        const status = await getMusicStatus(res.task_id);
                        setGenProgress(status.progress ?? 0);
                        setGenStep(status.message ?? '처리 중…');
                        if (status.status === 'succeeded') {
                            clearInterval(pollRef.current);
                            setIsGenerating(false);
                            const stems = JSON.parse(status.audio_url || '{}');
                            setStemModal({ stems });
                        } else if (status.status === 'failed') {
                            clearInterval(pollRef.current);
                            setIsGenerating(false);
                            setGenError(`스템 분리 실패: ${status.error ?? '알 수 없는 오류'}`);
                        }
                    } catch {
                        clearInterval(pollRef.current);
                        setIsGenerating(false);
                        setGenError('스템 분리 상태 조회 실패');
                    }
                }, 3000);
            }
        } catch {
            setIsGenerating(false);
            setGenError('12스템 분리에 실패했습니다');
        }
    };

    /* ── 타임스탬프 가사 핸들러 ── */
    const handleSyncedLyrics = async (track) => {
        if (!track.task_id || !track.suno_id) return;
        try {
            const result = await getTimestampedLyrics(track.task_id, track.suno_id);
            if (result?.alignedWords || result?.aligned_words) {
                setSyncedLyrics({
                    audioUrl: track.audio_url,
                    words: result.alignedWords || result.aligned_words,
                });
            }
        } catch {
            setGenError('타임스탬프 가사 조회에 실패했습니다');
        }
    };

    /* ── 스타일 부스트 핸들러 ── */
    const handleStyleBoost = async () => {
        if (!genre || styleBoostLoading) return;
        setStyleBoostLoading(true);
        try {
            const content = [
                GENRES.find(g => g.id === genre)?.label,
                ...moods.map(id => MOODS.find(m => m.id === id)?.label).filter(Boolean),
            ].join(', ');
            const result = await generateStyleBoost(content);
            if (result?.result) {
                setPrompt(result.result);
            }
        } catch {}
        finally { setStyleBoostLoading(false); }
    };

    /* ── 뮤직비디오 핸들러 ── */
    const handleVideoGenerate = async (track) => {
        if (!track.task_id || !track.suno_id || isGenerating) return;
        setTab('create');
        setIsGenerating(true);
        setTrack(null);
        setGenProgress(0);
        setGenStep('뮤직비디오 생성 요청 중…');
        setGenError(null);
        try {
            const res = await generateVideo({
                suno_task_id: track.task_id,
                suno_id: track.suno_id,
                track_id: track.id,
            });
            if (res?.task_id) {
                taskIdRef.current = res.task_id;
                startPolling(res.task_id, `${track.title} (Video)`);
            }
        } catch {
            setIsGenerating(false);
            setGenError('뮤직비디오 생성에 실패했습니다');
        }
    };

    const handleVideoProject = (track) => {
        setInitialTrackId(track.id);
        setTab('youtube');
    };

    const handleVideoPipeline = (track) => {
        setOpenPipelineFor(track.id);
        setTab('youtube');
    };

    const handleNewTrack = () => {
        setTrack(null);
        setGenProgress(0);
        setGenError(null);
        setCustomTitle('');
        clearInterval(pollRef.current);
    };

    const canGenerate = !!genre && !isGenerating;

    return (
        <div className="ms" style={{ '--ms-accent': accentColor }}>

            {/* ═══ HEADER ═══ */}
            <header className="ms-header">
                <div className="ms-header__left">
                    <p className="ms-header__kicker">AI · MUSIC · FORGE</p>
                    <h1 className="ms-header__title">
                        Sonic<br /><em>Forge</em>
                    </h1>
                    <p className="ms-header__desc">
                        AI가 당신의 아이디어를 완성된 음악으로 변환합니다.<br />
                        장르를 선택하고 감정을 담아 세상에 하나뿐인 트랙을 만드세요.
                    </p>
                </div>
                <div className="ms-header__right">
                    <CreditsBadge />
                    <SonicRadar isGenerating={isGenerating} accentColor={accentColor} />
                </div>
            </header>

            {/* ═══ TAB NAV ═══ */}
            <nav className="ms-tabs">
                <button
                    type="button"
                    className={`ms-tab ${tab === 'create' ? 'is-active' : ''}`}
                    onClick={() => setTab('create')}
                >
                    <span className="ms-tab__icon">⚗</span> Create
                </button>
                <button
                    type="button"
                    className={`ms-tab ${tab === 'lyrics' ? 'is-active' : ''}`}
                    onClick={() => setTab('lyrics')}
                >
                    <span className="ms-tab__icon">🎤</span> Lyrics
                </button>
                <button
                    type="button"
                    className={`ms-tab ${tab === 'library' ? 'is-active' : ''}`}
                    onClick={() => setTab('library')}
                >
                    <span className="ms-tab__icon">📚</span> Library
                    {library.length > 0 && (
                        <span className="ms-tab__badge">{library.length}</span>
                    )}
                </button>
                <button
                    type="button"
                    className={`ms-tab ${tab === 'remix' ? 'is-active' : ''}`}
                    onClick={() => setTab('remix')}
                >
                    <span className="ms-tab__icon">🔄</span> Remix
                </button>
                <button
                    type="button"
                    className={`ms-tab ms-tab--youtube ${tab === 'youtube' ? 'is-active' : ''}`}
                    onClick={() => setTab('youtube')}
                >
                    <span className="ms-tab__icon">🎯</span> YouTube
                </button>
            </nav>

            {/* ═══ LIBRARY TAB ═══ */}
            {tab === 'library' && (
                <PullToRefresh onRefresh={loadLibrary}>
                <Library
                    tracks={library}
                    loading={libLoading}
                    onDelete={handleDeleteFromLibrary}
                    onRefresh={loadLibrary}
                    onExtend={handleExtend}
                    onVocalRemoval={handleVocalRemoval}
                    onCoverArt={handleCoverArt}
                    onWavConvert={handleWavConvert}
                    onStemSplit={handleStemSplit}
                    onSyncedLyrics={handleSyncedLyrics}
                    onVideoGenerate={handleVideoGenerate}
                    onVideoProject={handleVideoProject}
                    onVideoPipeline={handleVideoPipeline}
                    isGenerating={isGenerating}
                />
                </PullToRefresh>
            )}

            {/* ═══ LYRICS TAB ═══ */}
            {tab === 'lyrics' && (
                <LyricsTab onUseInCreate={(text) => { setLyrics(text); setInstrumental(false); setProvider('suno'); setTab('create'); }} />
            )}

            {/* ═══ REMIX TAB ═══ */}
            {tab === 'remix' && (
                <RemixTab
                    onTaskStarted={(taskId, title) => {
                        setTab('create');
                        setIsGenerating(true);
                        setTrack(null);
                        setGenProgress(0);
                        setGenStep(`${title} 처리 중…`);
                        setGenError(null);
                        taskIdRef.current = taskId;
                        startPolling(taskId, title);
                    }}
                    model={model}
                    isGenerating={isGenerating}
                />
            )}

            {/* ═══ YOUTUBE TAB ═══ */}
            {tab === 'youtube' && (
                <YoutubeTab
                    library={library}
                    initialTrackId={initialTrackId}
                    onClearInitialTrack={() => setInitialTrackId(null)}
                    openPipelineFor={openPipelineFor}
                />
            )}

            {/* ═══ CREATE TAB ═══ */}
            {tab === 'create' && (
                <div className="ms-layout">

                    {/* ─── LEFT: Controls ─── */}
                    <div className="ms-controls">

                        {/* Provider Error */}
                        {providerError && (
                            <div className="ms-error-banner">
                                <span>⚠ 음악 서비스 연결 실패</span>
                                <button type="button" className="ms-btn ms-btn--ghost ms-btn--sm"
                                    onClick={() => {
                                        setProviderError(false);
                                        getMusicProviders()
                                            .then((data) => { setProviders(data.providers ?? []); setProviderError(false); })
                                            .catch(() => setProviderError(true));
                                    }}
                                >
                                    재시도
                                </button>
                            </div>
                        )}

                        {/* Provider Selector */}
                        {providers.length > 0 && (
                            <div className="ms-provider-bar">
                                {providers.map((p) => (
                                    <button
                                        key={p.id}
                                        type="button"
                                        className={`ms-provider-btn ${provider === p.id ? 'is-active' : ''}`}
                                        onClick={() => setProvider(p.id)}
                                    >
                                        <span className="ms-provider-btn__icon">
                                            {p.id === 'suno' ? '🎙️' : '🤖'}
                                        </span>
                                        <span className="ms-provider-btn__name">{p.name}</span>
                                        <span className="ms-provider-btn__desc">{p.description}</span>
                                    </button>
                                ))}
                            </div>
                        )}

                        {/* Model Selector (Suno only) */}
                        {provider === 'suno' && models.length > 0 && (
                            <div className="ms-model-bar">
                                <span className="ms-model-bar__label">Model</span>
                                <div className="ms-model-bar__options">
                                    {models.map((m) => (
                                        <button
                                            key={m.id}
                                            type="button"
                                            className={`ms-model-btn ${model === m.id ? 'is-active' : ''}`}
                                            onClick={() => setModel(m.id)}
                                            title={m.description || m.name}
                                        >
                                            {m.name}
                                        </button>
                                    ))}
                                </div>
                            </div>
                        )}

                        {/* Batch Generation Section */}
                        <details className="ms-batch-section" open={batchOpen} onToggle={(e) => setBatchOpen(e.currentTarget.open)}>
                            <summary>🎲 배치 생성 (장르 → 1-10트랙 + 자동 영상)</summary>
                            <div className="ms-batch-form">
                                <label>장르
                                    <select value={batchGenre} onChange={e => setBatchGenre(e.target.value)}>
                                        {batchGenresList.map(g => (
                                            <option key={g} value={g}>
                                                {g.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('-')}
                                            </option>
                                        ))}
                                    </select>
                                </label>
                                <label>트랙 수: <strong>{batchCount}</strong>
                                    <input type="range" min={1} max={10} value={batchCount}
                                        onChange={e => setBatchCount(parseInt(e.target.value))} />
                                </label>
                                <label>트랙당 길이: <strong>{batchDuration}초</strong>
                                    <input type="range" min={60} max={300} step={10} value={batchDuration}
                                        onChange={e => setBatchDuration(parseInt(e.target.value))} />
                                </label>
                                <label className="ms-batch-checkbox">
                                    <input type="checkbox" checked={batchAutoPipe}
                                        onChange={e => setBatchAutoPipe(e.target.checked)} />
                                    모든 트랙 생성 후 자동 영상 파이프라인 시작
                                </label>
                                <p className="ms-batch-estimate">
                                    예상: 약 {Math.ceil(batchCount * 1.5)}–{batchCount * 2}분 ·
                                    {' '}비용 ~${(batchCount * 0.005 + (batchAutoPipe ? 0.05 : 0)).toFixed(2)}
                                </p>
                                <button className="button primary" onClick={startBatch}
                                    disabled={batchPolling}>
                                    🎵 배치 생성 시작
                                </button>
                            </div>
                            {currentBatch && <BatchProgress batch={currentBatch} />}
                        </details>

                        {/* Step 1: Genre */}
                        <section className="ms-section">
                            <div className="ms-section__head">
                                <span className="ms-section__step">01</span>
                                <h2 className="ms-section__title">Genre</h2>
                                <span className="ms-section__hint">장르를 선택하세요</span>
                                {provider === 'suno' && (
                                    <button
                                        type="button"
                                        className={`ms-btn ms-btn--ghost ms-btn--sm ms-style-boost-btn ${styleBoostLoading ? 'is-loading' : ''}`}
                                        onClick={handleStyleBoost}
                                        disabled={styleBoostLoading || !genre}
                                        title="현재 설정으로 최적 스타일 프롬프트 생성"
                                    >
                                        {styleBoostLoading ? '생성 중...' : '✨ Style Boost'}
                                    </button>
                                )}
                                <button type="button" className={`ms-desc-toggle ${isOpen('genre') ? 'is-open' : ''}`} onClick={() => toggleDesc('genre')} aria-label="설명 펼치기">ℹ</button>
                            </div>
                            <div className={`ms-desc-wrap ${isOpen('genre') ? 'is-open' : ''}`}>
                                <p className="ms-section__desc">
                                    음악의 뼈대와 전체 사운드 방향을 결정합니다.<br />
                                    <strong>Lo-Fi</strong> — 따뜻한 바이닐 질감과 노이즈가 섞인 느슨한 비트<br />
                                    <strong>Electronic</strong> — 선명한 신스 레이어와 정밀한 시퀀스<br />
                                    <strong>Jazz</strong> — 즉흥적 화성 진행과 스윙 리듬<br />
                                    <strong>Classical</strong> — 현악·피아노 중심의 오케스트레이션<br />
                                    <strong>Ambient</strong> — 공간감 넘치는 드론과 패드 레이어<br />
                                    <strong>Hip-Hop</strong> — 샘플 기반의 펀치감 있는 비트<br />
                                    <strong>Rock</strong> — 기타 드라이브와 강한 다이나믹<br />
                                    <strong>Cinematic</strong> — 영화 스코어 스타일의 웅장한 편곡
                                </p>
                            </div>
                            <div className="ms-genre-grid">
                                {GENRES.map((g) => (
                                    <button
                                        key={g.id}
                                        type="button"
                                        className={`ms-genre-card ${genre === g.id ? 'is-active' : ''}`}
                                        style={{ '--g-color': g.color }}
                                        onClick={() => setGenre(g.id)}
                                        aria-pressed={genre === g.id}
                                    >
                                        <span className="ms-genre-card__icon">{g.icon}</span>
                                        <span className="ms-genre-card__label">{g.label}</span>
                                        <span className="ms-genre-card__desc">{g.desc}</span>
                                    </button>
                                ))}
                            </div>
                        </section>

                        {/* Step 2: Mood */}
                        <section className="ms-section">
                            <div className="ms-section__head">
                                <span className="ms-section__step">02</span>
                                <h2 className="ms-section__title">Mood</h2>
                                <span className="ms-section__hint">분위기 최대 3개</span>
                                <button type="button" className={`ms-desc-toggle ${isOpen('mood') ? 'is-open' : ''}`} onClick={() => toggleDesc('mood')} aria-label="설명 펼치기">ℹ</button>
                            </div>
                            <div className={`ms-desc-wrap ${isOpen('mood') ? 'is-open' : ''}`}>
                                <p className="ms-section__desc">
                                    음악의 감정 곡선과 에너지 밀도를 조절합니다. 최대 3개 조합 시 복합적인 감정 레이어가 만들어집니다.<br />
                                    <strong>Energetic</strong> — 강한 어택과 긴장감 있는 리듬, 빠른 전개<br />
                                    <strong>Chill</strong> — 여백이 많은 느슨한 편곡, 안정적인 흐름<br />
                                    <strong>Dark</strong> — 단조·불협화음 중심의 긴장감과 무게감<br />
                                    <strong>Uplifting</strong> — 상승하는 멜로디 라인과 밝은 화성 전개<br />
                                    <strong>Romantic</strong> — 부드러운 다이나믹과 풍부하고 따뜻한 화성<br />
                                    <strong>Epic</strong> — 광대한 오케스트레이션과 극적인 다이나믹 전개<br />
                                    <strong>Melancholic</strong> — 느린 전개의 감성적이고 내성적인 구성
                                </p>
                            </div>
                            <div className="ms-mood-rack">
                                {MOODS.map((m) => (
                                    <button
                                        key={m.id}
                                        type="button"
                                        className={`ms-mood-chip ${moods.includes(m.id) ? 'is-active' : ''}`}
                                        style={{ '--m-color': m.color }}
                                        onClick={() => toggleMood(m.id)}
                                        aria-pressed={moods.includes(m.id)}
                                    >
                                        {m.label}
                                    </button>
                                ))}
                            </div>
                        </section>

                        {/* Step 3: Instruments */}
                        <section className="ms-section">
                            <div className="ms-section__head">
                                <span className="ms-section__step">03</span>
                                <h2 className="ms-section__title">Instruments</h2>
                                <span className="ms-section__hint">원하는 악기 선택</span>
                                <button type="button" className={`ms-desc-toggle ${isOpen('instruments') ? 'is-open' : ''}`} onClick={() => toggleDesc('instruments')} aria-label="설명 펼치기">ℹ</button>
                            </div>
                            <div className={`ms-desc-wrap ${isOpen('instruments') ? 'is-open' : ''}`}>
                                <p className="ms-section__desc">
                                    선택한 악기가 실제 편곡 레이어를 구성합니다. 많을수록 풍성·밀도 있는 사운드, 적을수록 미니멀하고 깔끔한 결과물이 만들어집니다. 선택하지 않으면 장르 최적화 기본 구성으로 생성됩니다.<br />
                                    <strong>Piano</strong> — 화성 중심의 따뜻한 풍성함 (261Hz 중음역대)<br />
                                    <strong>Guitar</strong> — 리듬 질감과 어택감 (82Hz 저~중음역대)<br />
                                    <strong>Drums</strong> — 그루브와 타이밍의 뼈대 (60Hz 타격음)<br />
                                    <strong>Synth</strong> — 분위기를 감싸는 패드 레이어 (440Hz 넓은 스펙트럼)<br />
                                    <strong>Bass</strong> — 저음의 그라운드, 리듬감 강화 (41Hz 저음역)<br />
                                    <strong>Strings</strong> — 감성적 현악 레이어와 서정성 (196Hz)<br />
                                    <strong>Brass</strong> — 펀치감과 힘, 금관 질감 (146Hz)<br />
                                    <strong>Flute · Violin</strong> — 섬세하고 표현적인 멜로디 라인<br />
                                    <strong>Choir</strong> — 공간감과 웅장함, 인간적인 온기
                                </p>
                            </div>
                            <div className="ms-instrument-rack">
                                {INSTRUMENTS.map((inst) => (
                                    <button
                                        key={inst.id}
                                        type="button"
                                        className={`ms-instrument-chip ${instruments.includes(inst.id) ? 'is-active' : ''}`}
                                        onClick={() => toggleInstrument(inst.id)}
                                        aria-pressed={instruments.includes(inst.id)}
                                    >
                                        <span className="ms-instrument-chip__label">{inst.label}</span>
                                        <span className="ms-instrument-chip__freq">{inst.freq}</span>
                                    </button>
                                ))}
                            </div>
                        </section>

                        {/* Step 4: Parameters */}
                        <section className="ms-section">
                            <div className="ms-section__head">
                                <span className="ms-section__step">04</span>
                                <h2 className="ms-section__title">Parameters</h2>
                                <span className="ms-section__hint">음악 파라미터 설정</span>
                            </div>

                            {/* Duration */}
                            <div className="ms-param-group">
                                <div className="ms-param-label-row">
                                    <label className="ms-param-label">Duration</label>
                                    <button type="button" className={`ms-desc-toggle ms-desc-toggle--sm ${isOpen('duration') ? 'is-open' : ''}`} onClick={() => toggleDesc('duration')} aria-label="설명 펼치기">?</button>
                                </div>
                                <div className={`ms-desc-wrap ${isOpen('duration') ? 'is-open' : ''}`}>
                                    <p className="ms-param-hint">30초~1분은 인트로·짧은 BGM, 2~3분은 유튜브 단독 콘텐츠, 5분은 장시간 재생용 환경음악에 최적입니다.</p>
                                </div>
                                <div className="ms-duration-rail">
                                    {DURATIONS.map((d) => (
                                        <button
                                            key={d.id}
                                            type="button"
                                            className={`ms-duration-btn ${duration === d.id ? 'is-active' : ''}`}
                                            onClick={() => setDuration(d.id)}
                                        >
                                            {d.label}
                                        </button>
                                    ))}
                                </div>
                            </div>

                            {/* BPM */}
                            <div className="ms-param-group">
                                <div className="ms-param-row">
                                    <div className="ms-param-label-row">
                                        <label className="ms-param-label">BPM</label>
                                        <button type="button" className={`ms-desc-toggle ms-desc-toggle--sm ${isOpen('bpm') ? 'is-open' : ''}`} onClick={() => toggleDesc('bpm')} aria-label="설명 펼치기">?</button>
                                    </div>
                                    <span className="ms-param-value">{bpm}</span>
                                </div>
                                <div className={`ms-desc-wrap ${isOpen('bpm') ? 'is-open' : ''}`}>
                                    <p className="ms-param-hint">Slow(60–80)는 명상·환경음악, Mid(90–110)는 카페·집중 BGM, Fast(120–140)는 운동·에너제틱 무드, EDM(150+)는 댄스플로어 에너지에 적합합니다. 슬라이더로 1 BPM 단위 정밀 조정이 가능합니다.</p>
                                </div>
                                <div className="ms-bpm-presets">
                                    {BPM_PRESETS.map((p) => (
                                        <button
                                            key={p.label}
                                            type="button"
                                            className={`ms-bpm-preset ${bpm === p.bpm ? 'is-active' : ''}`}
                                            onClick={() => setBpm(p.bpm)}
                                        >
                                            {p.label} <span>{p.bpm}</span>
                                        </button>
                                    ))}
                                </div>
                                <input
                                    type="range" min={40} max={200} value={bpm}
                                    onChange={(e) => setBpm(Number(e.target.value))}
                                    className="ms-bpm-slider"
                                    aria-label="BPM"
                                />
                            </div>

                            {/* Key + Scale */}
                            <div className="ms-param-grid">
                                <div className="ms-param-group">
                                    <div className="ms-param-label-row">
                                        <label className="ms-param-label">Key</label>
                                        <button type="button" className={`ms-desc-toggle ms-desc-toggle--sm ${isOpen('key') ? 'is-open' : ''}`} onClick={() => toggleDesc('key')} aria-label="설명 펼치기">?</button>
                                    </div>
                                    <div className={`ms-desc-wrap ${isOpen('key') ? 'is-open' : ''}`}>
                                        <p className="ms-param-hint">C·G는 밝고 자연스러운 울림, D·A는 따뜻하고 공명하는 음색, F#·B♭는 재즈적 긴장감을 만들어냅니다.</p>
                                    </div>
                                    <div className="ms-select-wrap">
                                        <select
                                            className="ms-select"
                                            value={musicalKey}
                                            onChange={(e) => setMusicalKey(e.target.value)}
                                        >
                                            {KEYS.map((k) => (
                                                <option key={k} value={k}>{k}</option>
                                            ))}
                                        </select>
                                    </div>
                                </div>
                                <div className="ms-param-group">
                                    <div className="ms-param-label-row">
                                        <label className="ms-param-label">Scale</label>
                                        <button type="button" className={`ms-desc-toggle ms-desc-toggle--sm ${isOpen('scale') ? 'is-open' : ''}`} onClick={() => toggleDesc('scale')} aria-label="설명 펼치기">?</button>
                                    </div>
                                    <div className={`ms-desc-wrap ${isOpen('scale') ? 'is-open' : ''}`}>
                                        <p className="ms-param-hint">Major는 밝고 긍정적, Minor는 감성적·우울, Dorian은 재즈풍, Phrygian은 어둡고 이국적, Lydian은 몽환적·부유감, Mixolydian은 블루지·록 감성입니다.</p>
                                    </div>
                                    <div className="ms-select-wrap">
                                        <select
                                            className="ms-select"
                                            value={scale}
                                            onChange={(e) => setScale(e.target.value)}
                                        >
                                            {SCALES.map((s) => (
                                                <option key={s} value={s}>{s}</option>
                                            ))}
                                        </select>
                                    </div>
                                </div>
                            </div>

                            {/* Vocal Gender (Suno only) */}
                            {provider === 'suno' && (
                                <div className="ms-param-group">
                                    <label className="ms-param-label">Vocal Gender</label>
                                    <div className="ms-gender-toggle">
                                        {[
                                            { value: null, label: 'Auto', icon: '🎵' },
                                            { value: 'm', label: 'Male', icon: '♂' },
                                            { value: 'f', label: 'Female', icon: '♀' },
                                        ].map((opt) => (
                                            <button
                                                key={opt.label}
                                                type="button"
                                                className={`ms-gender-btn ${vocalGender === opt.value ? 'is-active' : ''} ${opt.value === 'm' ? 'is-male' : opt.value === 'f' ? 'is-female' : ''}`}
                                                onClick={() => setVocalGender(opt.value)}
                                            >
                                                <span className="ms-gender-btn__icon">{opt.icon}</span>
                                                {opt.label}
                                            </button>
                                        ))}
                                    </div>
                                </div>
                            )}

                            {/* Negative Tags (Suno only) */}
                            {provider === 'suno' && (
                                <div className="ms-param-group">
                                    <label className="ms-param-label">Exclude Styles</label>
                                    <div className="ms-negative-tags">
                                        <div className="ms-negative-tags__presets">
                                            {['screaming', 'autotune', 'distortion', 'whisper', 'falsetto', 'rap'].map((tag) => (
                                                <button
                                                    key={tag}
                                                    type="button"
                                                    className={`ms-neg-chip ${negativeTags.includes(tag) ? 'is-active' : ''}`}
                                                    onClick={() => {
                                                        setNegativeTags((prev) => {
                                                            const tags = prev.split(',').map(t => t.trim()).filter(Boolean);
                                                            if (tags.includes(tag)) return tags.filter(t => t !== tag).join(', ');
                                                            return [...tags, tag].join(', ');
                                                        });
                                                    }}
                                                >
                                                    {tag}
                                                </button>
                                            ))}
                                        </div>
                                        <input
                                            type="text"
                                            className="ms-negative-tags__input"
                                            placeholder="추가로 제외할 스타일을 입력..."
                                            value={negativeTags}
                                            onChange={(e) => setNegativeTags(e.target.value)}
                                        />
                                    </div>
                                </div>
                            )}

                            {/* Style Weight / Audio Weight (Suno only) */}
                            {provider === 'suno' && (
                                <div className="ms-param-grid">
                                    <div className="ms-param-group">
                                        <div className="ms-param-row">
                                            <label className="ms-param-label">Style Weight</label>
                                            <span className="ms-param-value">{styleWeight}%</span>
                                        </div>
                                        <p className="ms-param-hint ms-param-hint--inline">Prompt ↔ Style 밸런스</p>
                                        <input
                                            type="range" min={0} max={100} value={styleWeight}
                                            onChange={(e) => setStyleWeight(Number(e.target.value))}
                                            className="ms-bpm-slider"
                                            aria-label="Style Weight"
                                        />
                                    </div>
                                    <div className="ms-param-group">
                                        <div className="ms-param-row">
                                            <label className="ms-param-label">Audio Weight</label>
                                            <span className="ms-param-value">{audioWeight}%</span>
                                        </div>
                                        <p className="ms-param-hint ms-param-hint--inline">Original ↔ AI 밸런스</p>
                                        <input
                                            type="range" min={0} max={100} value={audioWeight}
                                            onChange={(e) => setAudioWeight(Number(e.target.value))}
                                            className="ms-bpm-slider"
                                            aria-label="Audio Weight"
                                        />
                                    </div>
                                </div>
                            )}
                        </section>

                        {/* Step 5: Prompt */}
                        <section className="ms-section">
                            <div className="ms-section__head">
                                <span className="ms-section__step">05</span>
                                <h2 className="ms-section__title">Creative Brief</h2>
                                <span className="ms-section__hint">선택사항</span>
                                <button type="button" className={`ms-desc-toggle ${isOpen('prompt') ? 'is-open' : ''}`} onClick={() => toggleDesc('prompt')} aria-label="설명 펼치기">ℹ</button>
                            </div>
                            <div className={`ms-desc-wrap ${isOpen('prompt') ? 'is-open' : ''}`}>
                                <p className="ms-section__desc">
                                    AI가 생성할 음악의 방향을 자유롭게 묘사하는 텍스트 프롬프트입니다. 위 파라미터와 결합되어 작동하며, 상충할 경우 파라미터 설정이 우선합니다.<br />
                                    구체적인 장면·감정·레퍼런스 아티스트·분위기 키워드를 포함할수록 더 타겟팅된 결과물이 만들어집니다.<br />
                                    <em>"새벽 4시 도시의 텅 빈 도로, 빗소리와 함께 흐르는 서정적인 피아노"</em><br />
                                    <em>"Nils Frahm 스타일의 미니멀 피아노, 전자음과 어쿠스틱의 경계"</em>
                                </p>
                            </div>
                            <div className="ms-prompt-wrap">
                                <textarea
                                    className="ms-prompt"
                                    placeholder="원하는 음악을 자유롭게 묘사하세요. 예: 새벽 카페에서 커피를 마시며 읽는 책의 분위기, 빗소리가 배경으로 깔리는 재즈…"
                                    value={prompt}
                                    onChange={(e) => setPrompt(e.target.value)}
                                    rows={4}
                                    maxLength={500}
                                />
                                <span className="ms-prompt__count">{prompt.length}/500</span>
                            </div>
                        </section>

                        {/* Step 6: Vocals & Lyrics (Suno only) */}
                        {provider === 'suno' && (
                            <section className="ms-section">
                                <div className="ms-section__head">
                                    <span className="ms-section__step">06</span>
                                    <h2 className="ms-section__title">Vocals & Lyrics</h2>
                                    <span className="ms-section__hint">Suno 전용</span>
                                </div>

                                <div className="ms-vocal-toggle">
                                    <button
                                        type="button"
                                        className={`ms-vocal-btn ${!instrumental ? 'is-active' : ''}`}
                                        onClick={() => setInstrumental(false)}
                                    >
                                        🎤 보컬 포함
                                    </button>
                                    <button
                                        type="button"
                                        className={`ms-vocal-btn ${instrumental ? 'is-active' : ''}`}
                                        onClick={() => setInstrumental(true)}
                                    >
                                        🎹 인스트루멘탈
                                    </button>
                                </div>

                                {!instrumental && (
                                    <>
                                        <div className="ms-lyrics-wrap">
                                            <div className="ms-lyrics-header">
                                                <label className="ms-param-label">가사</label>
                                                <button
                                                    type="button"
                                                    className="ms-btn ms-btn--ghost ms-btn--sm"
                                                    onClick={handleGenerateLyrics}
                                                    disabled={lyricsLoading || (!prompt && !genre)}
                                                >
                                                    {lyricsLoading ? '생성 중...' : '✨ AI 가사 생성'}
                                                </button>
                                            </div>
                                            <textarea
                                                className="ms-lyrics"
                                                placeholder={"[Verse]\n여기에 가사를 입력하세요...\n\n[Chorus]\n후렴구 가사...\n\n비워두면 AI가 자동으로 가사를 작성합니다."}
                                                value={lyrics}
                                                onChange={(e) => setLyrics(e.target.value)}
                                                rows={8}
                                            />
                                            <p className="ms-lyrics-hint">
                                                섹션 태그: [Verse], [Chorus], [Bridge], [Outro], [Instrumental]
                                            </p>
                                        </div>
                                    </>
                                )}
                            </section>
                        )}
                    </div>

                    {/* ─── RIGHT: Stage ─── */}
                    <div className="ms-stage">
                        {/* Stage waveform */}
                        <div className="ms-stage__viz">
                            <WaveformCanvas isGenerating={isGenerating} accentColor={accentColor} />
                            <div className="ms-stage__overlay">
                                {!genre && !isGenerating && !track && (
                                    <p className="ms-stage__idle">장르를 선택하면<br />여기서 음악이 깨어납니다</p>
                                )}
                                {genre && !isGenerating && !track && (
                                    <div className="ms-stage__ready">
                                        <span className="ms-stage__ready-icon">{activeGenre?.icon}</span>
                                        <p className="ms-stage__ready-label">{activeGenre?.label}</p>
                                        {moods.length > 0 && (
                                            <p className="ms-stage__ready-moods">
                                                {moods.map((id) => MOODS.find((m) => m.id === id)?.label).join(' · ')}
                                            </p>
                                        )}
                                    </div>
                                )}
                            </div>
                        </div>

                        {/* Track title input */}
                        {!track && (
                            <div className="ms-title-input-wrap">
                                <input
                                    type="text"
                                    className="ms-title-input"
                                    placeholder="트랙 제목 (비워두면 자동 생성)"
                                    value={customTitle}
                                    onChange={(e) => setCustomTitle(e.target.value)}
                                    maxLength={80}
                                />
                            </div>
                        )}

                        {/* Generate button */}
                        {!track && (
                            <button
                                type="button"
                                className={`ms-generate-btn ${canGenerate ? 'is-ready' : ''} ${isGenerating ? 'is-generating' : ''}`}
                                onClick={handleGenerate}
                                disabled={!canGenerate}
                            >
                                <span className="ms-generate-btn__ring" aria-hidden />
                                <span className="ms-generate-btn__core">
                                    {isGenerating ? (
                                        <span className="ms-generate-btn__spinner" aria-hidden />
                                    ) : (
                                        <svg width="28" height="28" viewBox="0 0 28 28" fill="none" aria-hidden>
                                            <path d="M14 4v20M4 14h20" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
                                            <circle cx="14" cy="14" r="12" stroke="currentColor" strokeWidth="1.5" opacity="0.4"/>
                                        </svg>
                                    )}
                                </span>
                                <span className="ms-generate-btn__label">
                                    {isGenerating ? 'Generating…' : `Generate Track${provider === 'suno' ? ' (Suno)' : ''}`}
                                </span>
                            </button>
                        )}

                        {/* Progress */}
                        {isGenerating && (
                            <GenerationProgress progress={genProgress} stepMsg={genStep} />
                        )}

                        {/* Error */}
                        {genError && !isGenerating && (
                            <div className="ms-gen-error">
                                <span>⚠ {genError}</span>
                                <button type="button" className="ms-btn ms-btn--ghost" onClick={handleNewTrack}>
                                    재시도
                                </button>
                            </div>
                        )}

                        {/* Result */}
                        {track && !isGenerating && (
                            <TrackResult
                                track={track}
                                onNew={handleNewTrack}
                            />
                        )}

                        {/* Spec chips */}
                        {genre && !track && !isGenerating && (
                            <div className="ms-stage__spec">
                                <div className="ms-spec-chip">
                                    <span className="ms-spec-chip__label">Genre</span>
                                    <span className="ms-spec-chip__val">{activeGenre?.label}</span>
                                </div>
                                <div className="ms-spec-chip">
                                    <span className="ms-spec-chip__label">Duration</span>
                                    <span className="ms-spec-chip__val">
                                        {DURATIONS.find((d) => d.id === duration)?.label}
                                    </span>
                                </div>
                                <div className="ms-spec-chip">
                                    <span className="ms-spec-chip__label">BPM</span>
                                    <span className="ms-spec-chip__val">{bpm}</span>
                                </div>
                                <div className="ms-spec-chip">
                                    <span className="ms-spec-chip__label">Key</span>
                                    <span className="ms-spec-chip__val">{musicalKey} {scale}</span>
                                </div>
                                {instruments.length > 0 && (
                                    <div className="ms-spec-chip">
                                        <span className="ms-spec-chip__label">Instruments</span>
                                        <span className="ms-spec-chip__val">{instruments.length}개</span>
                                    </div>
                                )}
                            </div>
                        )}
                    </div>
                </div>
            )}

            {coverArtModal && (
                <CoverArtModal
                    images={coverArtModal.images}
                    onSelect={handleCoverSelect}
                    onClose={() => setCoverArtModal(null)}
                />
            )}

            {/* ═══ Stem Modal ═══ */}
            {stemModal && (
                <StemModal stems={stemModal.stems} onClose={() => setStemModal(null)} />
            )}

            {/* ═══ Synced Lyrics Player ═══ */}
            {syncedLyrics && (
                <SyncedLyricsPlayer
                    audioUrl={syncedLyrics.audioUrl}
                    alignedWords={syncedLyrics.words}
                    onClose={() => setSyncedLyrics(null)}
                    accentColor={accentColor}
                />
            )}

            {tab === 'library' && (
                <FAB onClick={() => setTab('create')} label="음악 생성" />
            )}
        </div>
    );
}
