/* ===========================================================================
   iRaven App Store Studio — MiniMax media suite (full model coverage).
   Every MiniMax modality + model is selectable, and each model exposes its own
   inputs: image (aspect/count/subject-ref), video (text→video, image→video with
   first/last frame, subject reference, duration, resolution, camera-director
   prompts), music (style + lyrics), and voice (model, voice, emotion, speed,
   pitch, volume, format). Requests go through the Docker proxy (/api/minimax)
   when present, else straight to the API with the configured key.
   =========================================================================== */

function mmCheck(j) {
  if (j && j.base_resp && j.base_resp.status_code !== 0) {
    throw new Error(j.base_resp.status_msg || ('MiniMax error ' + j.base_resp.status_code));
  }
  if (j && j.error) throw new Error(typeof j.error === 'string' ? j.error : JSON.stringify(j.error));
  return j;
}

async function mmRequest(opts) {
  const cfg = aiCfg();
  if (window.__hasBackend) {
    const r = await fetch('/api/minimax', { method: 'POST', headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ ...opts, key: cfg.key, baseUrl: cfg.baseUrl }) });
    if (opts.download) { if (!r.ok) throw new Error('download failed (' + r.status + ')'); return await r.blob(); }
    return mmCheck(await r.json());
  }
  if (!cfg.key) throw new Error('Media generation needs a MiniMax API key — add one in AI settings (⚙).');
  if (opts.download) {
    const r = await fetch(opts.download);
    if (!r.ok) throw new Error('download failed (' + r.status + ')');
    return await r.blob();
  }
  const base = (cfg.baseUrl || 'https://api.minimax.io/v1').replace(/\/$/, '');
  const qs = opts.query ? '?' + new URLSearchParams(opts.query) : '';
  const r = await fetch(base + opts.path + qs, {
    method: opts.method || 'POST',
    headers: { 'content-type': 'application/json', authorization: 'Bearer ' + cfg.key },
    body: (opts.method === 'GET') ? undefined : JSON.stringify(opts.payload || {}),
  });
  return mmCheck(await r.json());
}

function mmHexToBlob(hex, type) {
  const bytes = new Uint8Array(hex.length / 2);
  for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
  return new Blob([bytes], { type });
}
const mmBlobToDataURL = (blob) => new Promise(r => { const f = new FileReader(); f.onload = () => r(f.result); f.readAsDataURL(blob); });

/* a storage asset (or bundled path) → base64 data URL for image-input params */
async function assetToDataURL(asset) {
  if (!asset) return null;
  if (asset.path && !asset.key) { const r = await fetch(window.resolveAsset(asset.path)); return await mmBlobToDataURL(await r.blob()); }
  const blob = await window.vidMediaGet(asset.key);
  return blob ? await mmBlobToDataURL(blob) : null;
}

/* ---------- generators (payload-driven) ---------- */
async function mmGenImage(payload) {
  const j = await mmRequest({ path: '/image_generation', payload: { response_format: 'base64', ...payload } });
  const d = j.data || {};
  let urls = [];
  if (d.image_base64 && d.image_base64.length) urls = d.image_base64.map(b => 'data:image/png;base64,' + b);
  else if (d.image_urls && d.image_urls.length) {
    for (const u of d.image_urls) urls.push(await mmBlobToDataURL(await mmRequest({ download: u })));
  }
  if (!urls.length) throw new Error('no image in response');
  return urls;
}

async function mmGenVideo(payload, onStatus) {
  const j = await mmRequest({ path: '/video_generation', payload });
  const taskId = j.task_id;
  if (!taskId) throw new Error('no task_id in response');
  for (let i = 0; i < 165; i++) {                        // up to ~11 min
    await new Promise(r => setTimeout(r, 4000));
    const q = await mmRequest({ path: '/query/video_generation', method: 'GET', query: { task_id: taskId } });
    const s = (q.status || '').toLowerCase();
    onStatus && onStatus(`Video: ${s || 'processing'}… (${Math.round(i * 4 / 60)}m)`);
    if (s === 'success') {
      const f = await mmRequest({ path: '/files/retrieve', method: 'GET', query: { file_id: q.file_id } });
      const url = f.file && (f.file.download_url || f.file.url);
      if (!url) throw new Error('no download_url for generated video');
      onStatus && onStatus('Downloading clip…');
      return await mmRequest({ download: url });
    }
    if (s === 'fail' || s === 'failed') throw new Error(q.error_message || 'video generation failed');
  }
  throw new Error('video generation timed out');
}

async function mmGenMusic(payload) {
  const j = await mmRequest({ path: '/music_generation', payload });
  const d = j.data || {};
  if (d.audio) return mmHexToBlob(d.audio, 'audio/mpeg');
  if (d.audio_url) return await mmRequest({ download: d.audio_url });
  throw new Error('no audio in response');
}

async function mmGenVoice(payload) {
  const j = await mmRequest({ path: '/t2a_v2', payload });
  const d = j.data || {};
  if (d.audio) return mmHexToBlob(d.audio, payload.audio_setting && payload.audio_setting.format === 'wav' ? 'audio/wav' : 'audio/mpeg');
  throw new Error('no audio in response');
}

/* ---------- model registry ---------- */
const MM_IMAGE_MODELS = [{ id: 'image-01', label: 'image-01 — text → image' }];
const MM_ASPECTS = ['1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3', '21:9'];

const MM_VIDEO_MODELS = [
  { id: 'MiniMax-Hailuo-2.3', label: 'Hailuo 2.3 — newest, cinematic', firstFrame: true, lastFrame: true, durations: [6, 10], resolutions: ['768P', '1080P'] },
  { id: 'MiniMax-Hailuo-02', label: 'Hailuo 02 — 1080p, physics', firstFrame: true, lastFrame: true, durations: [6, 10], resolutions: ['512P', '768P', '1080P'] },
  { id: 'T2V-01-Director', label: 'T2V-01 Director — camera moves', director: true },
  { id: 'T2V-01', label: 'T2V-01 — text → video' },
  { id: 'I2V-01-Director', label: 'I2V-01 Director — image + camera', firstFrame: true, director: true },
  { id: 'I2V-01', label: 'I2V-01 — image → video', firstFrame: true },
  { id: 'I2V-01-live', label: 'I2V-01 Live — anime / illustration', firstFrame: true },
  { id: 'S2V-01', label: 'S2V-01 — subject reference (face)', subject: true },
];
const mmVideoModel = (id) => MM_VIDEO_MODELS.find(m => m.id === id) || MM_VIDEO_MODELS[0];

const MM_MUSIC_MODELS = [{ id: 'music-1.5', label: 'music-1.5' }, { id: 'music-01', label: 'music-01' }];

const MM_VOICE_MODELS = [
  { id: 'speech-2.5-hd-preview', label: 'speech-2.5 HD (preview)' },
  { id: 'speech-2.5-turbo-preview', label: 'speech-2.5 Turbo (preview)' },
  { id: 'speech-02-hd', label: 'speech-02 HD' },
  { id: 'speech-02-turbo', label: 'speech-02 Turbo' },
  { id: 'speech-01-hd', label: 'speech-01 HD' },
  { id: 'speech-01-turbo', label: 'speech-01 Turbo' },
];
const MM_VOICES = [
  'English_expressive_narrator', 'English_radiant_girl', 'English_CalmWoman', 'English_Graceful_Lady',
  'English_ManWithDeepVoice', 'English_Wiselady', 'English_FriendlyPerson', 'English_Trustworth_Man',
  'English_magnetic_voiced_man', 'English_Aussie_Bloke', 'English_captivating_storyteller',
  'English_PlayfulGirl', 'English_Gentle-voiced_man', 'Spanish_Narrator', 'French_Narrator',
  'Portuguese_Narrator', 'German_Narrator', 'Arabic_CalmWoman',
];
const MM_EMOTIONS = ['auto', 'neutral', 'happy', 'sad', 'angry', 'fearful', 'disgusted', 'surprised'];
const MM_AUDIO_FORMATS = ['mp3', 'wav', 'pcm'];

/* ---------- small controls ---------- */
function MMRow({ label, hint, children }) {
  return (
    <div style={{ marginBottom: 12 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 5 }}>
        <span style={{ fontSize: 11.5, fontWeight: 600, color: 'rgba(255,255,255,0.55)', fontFamily: BODY }}>{label}</span>
        {hint ? <span style={{ fontSize: 10.5, color: 'rgba(255,255,255,0.35)', fontFamily: "'JetBrains Mono',monospace" }}>{hint}</span> : null}
      </div>
      {children}
    </div>
  );
}
function MMSelect({ value, onChange, options }) {
  return (
    <select value={value} onChange={e => onChange(e.target.value)} style={{ width: '100%', boxSizing: 'border-box',
      padding: '9px 10px', borderRadius: 9, cursor: 'pointer', background: 'rgba(255,255,255,0.05)',
      border: '1px solid rgba(255,255,255,0.14)', color: '#fff', fontFamily: BODY, fontSize: 13, outline: 'none', appearance: 'none' }}>
      {options.map(o => { const id = (typeof o === 'object') ? o.id : o; const lab = (typeof o === 'object') ? o.label : o;
        return <option key={String(id)} value={id} style={{ background: '#0c1222' }}>{lab}</option>; })}
    </select>
  );
}
function MMSlider({ value, min, max, step, onChange, fmt }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
      <input type="range" min={min} max={max} step={step} value={value} onChange={e => onChange(+e.target.value)}
        style={{ flex: 1, accentColor: '#3b78ff' }} />
      <span style={{ fontFamily: "'JetBrains Mono',monospace", fontSize: 12, color: '#9dbcff', minWidth: 38, textAlign: 'right' }}>
        {fmt ? fmt(value) : value}</span>
    </div>
  );
}

/* inline image-reference picker (storage images + upload) — avoids modal z-index clashes */
function MMImageInput({ label, hint, asset, assets, setAssets, onPick }) {
  const [open, setOpen] = useState(false);
  const fileRef = useRef(null);
  const imgs = (assets || []).filter(a => a.kind === 'image');
  return (
    <MMRow label={label} hint={hint}>
      <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
        <div style={{ width: 54, height: 54, borderRadius: 9, overflow: 'hidden', flex: '0 0 auto', background: '#05070f',
          border: '1px solid rgba(255,255,255,0.12)' }}>
          {asset ? <window.MediaThumb value={window.assetToRef(asset)} /> : (
            <div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center',
              color: 'rgba(255,255,255,0.3)', fontSize: 18 }}>＋</div>)}
        </div>
        <button style={btnStyle({ padding: '8px 12px', fontSize: 12.5 })} onClick={() => setOpen(o => !o)}>
          {asset ? 'Change' : 'Choose'}</button>
        <button style={btnStyle({ padding: '8px 12px', fontSize: 12.5 })} onClick={() => fileRef.current.click()}>Upload</button>
        {asset ? <button style={btnStyle({ padding: '8px 10px', fontSize: 12.5, color: '#ff8089' })} onClick={() => onPick(null)}>✕</button> : null}
        <input ref={fileRef} type="file" accept="image/*" style={{ display: 'none' }}
          onChange={async e => { const fl = e.target.files[0]; e.target.value = ''; if (!fl) return;
            const a = await window.storeAsset(setAssets, { blob: fl, kind: 'image', source: 'upload', name: fl.name }); onPick(a); setOpen(false); }} />
      </div>
      {open ? (
        <div style={{ marginTop: 8, padding: 8, borderRadius: 10, background: 'rgba(0,0,0,0.3)',
          border: '1px solid rgba(255,255,255,0.10)', maxHeight: 180, overflowY: 'auto' }}>
          {imgs.length ? (
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(56px,1fr))', gap: 6 }}>
              {imgs.map(a => (
                <div key={a.id} onClick={() => { onPick(a); setOpen(false); }} title={a.name}
                  style={{ aspectRatio: '1', borderRadius: 8, overflow: 'hidden', cursor: 'pointer',
                    border: asset && asset.id === a.id ? '2px solid #2f6df6' : '1px solid rgba(255,255,255,0.1)' }}>
                  <window.MediaThumb value={window.assetToRef(a)} />
                </div>
              ))}
            </div>
          ) : <div style={{ fontSize: 12, color: 'rgba(255,255,255,0.4)', padding: 6 }}>No images in storage — upload one.</div>}
        </div>
      ) : null}
    </MMRow>
  );
}

/* ---------- modal ---------- */
const MM_TABS = [
  { id: 'image', label: 'Image' }, { id: 'video', label: 'Video' },
  { id: 'music', label: 'Music' }, { id: 'voice', label: 'Voice' },
];

function AIMediaModal({ context, initial, onUse, assets, setAssets, onClose, onStatus }) {
  const [mtab, setMtab] = useState((initial && initial.kind) || 'image');
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const [result, setResult] = useState(null);
  const [saved, setSaved] = useState(null);

  const [F, setF] = useState({
    // image
    iPrompt: (initial && initial.kind === 'image' && initial.prompt) || '', iModel: 'image-01', iAspect: '9:16', iN: 1, iOpt: true, iSubject: null,
    // video
    vPrompt: (initial && initial.kind === 'video' && initial.prompt) || '', vModel: 'MiniMax-Hailuo-2.3',
    vDur: 6, vRes: '1080P', vOpt: true, vFirst: null, vLast: null, vSubject: null,
    // music
    mPrompt: (initial && initial.kind === 'music' && initial.prompt) || '', mLyrics: '', mModel: 'music-1.5',
    // voice
    tText: '', tModel: 'speech-02-hd', tVoice: 'English_expressive_narrator', tEmotion: 'auto',
    tSpeed: 1, tVol: 1, tPitch: 0, tFormat: 'mp3',
  });
  const set = (k) => (v) => setF(s => ({ ...s, [k]: v }));
  const vModel = mmVideoModel(F.vModel);

  /* keep duration/resolution valid when switching video model */
  useEffect(() => {
    setF(s => {
      const m = mmVideoModel(s.vModel); const u = {};
      if (m.durations && !m.durations.includes(s.vDur)) u.vDur = m.durations[0];
      if (m.resolutions && !m.resolutions.includes(s.vRes)) u.vRes = m.resolutions[m.resolutions.length - 1];
      return Object.keys(u).length ? { ...s, ...u } : s;
    });
  }, [F.vModel]);

  const promptOf = () => ({ image: F.iPrompt, video: F.vPrompt, music: F.mPrompt, voice: F.tText }[mtab] || '');

  const saveOne = async (res) => {
    const kind = res.kind === 'audio' ? 'audio' : res.kind;
    const name = 'ai-' + (res.sub || res.kind) + '-' + Date.now().toString(36).slice(-4) +
      (kind === 'image' ? '.png' : kind === 'video' ? '.mp4' : (res.fmt === 'wav' ? '.wav' : '.mp3'));
    return await window.storeAsset(setAssets, { kind, blob: res.blob, dataUrl: res.dataUrl,
      source: 'ai', sub: res.sub || mtab, name, prompt: promptOf() });
  };

  const run = async () => {
    if (busy) return;
    setErr(null); setBusy(true); setResult(null); setSaved(null);
    try {
      let res, firstAsset;
      if (mtab === 'image') {
        const payload = { model: F.iModel, prompt: F.iPrompt, aspect_ratio: F.iAspect, n: F.iN, prompt_optimizer: F.iOpt };
        if (F.iSubject) { const du = await assetToDataURL(F.iSubject); if (du) payload.subject_reference = [{ type: 'character', image_file: du }]; }
        const urls = await mmGenImage(payload);
        onStatus && onStatus('Saving to storage…');
        const assetsOut = [];
        for (const u of urls) assetsOut.push(await saveOne({ kind: 'image', dataUrl: u, sub: 'image' }));
        firstAsset = assetsOut[0];
        res = { kind: 'image', dataUrl: urls[0], count: urls.length };
      } else if (mtab === 'video') {
        const payload = { model: F.vModel, prompt: F.vPrompt };
        if (vModel.durations) payload.duration = F.vDur;
        if (vModel.resolutions) payload.resolution = F.vRes;
        if (vModel.firstFrame || vModel.id.indexOf('I2V') === 0) payload.prompt_optimizer = F.vOpt;
        else payload.prompt_optimizer = F.vOpt;
        if ((vModel.firstFrame || vModel.id.indexOf('I2V') === 0) && F.vFirst) payload.first_frame_image = await assetToDataURL(F.vFirst);
        if (vModel.lastFrame && F.vLast) payload.last_frame_image = await assetToDataURL(F.vLast);
        if (vModel.subject && F.vSubject) { const du = await assetToDataURL(F.vSubject); if (du) payload.subject_reference = [{ type: 'character', image: [du] }]; }
        const blob = await mmGenVideo(payload, s => { setErr(null); onStatus && onStatus(s); });
        res = { kind: 'video', blob, url: URL.createObjectURL(blob), sub: 'video' };
        onStatus && onStatus('Saving to storage…'); firstAsset = await saveOne(res);
      } else if (mtab === 'music') {
        const ly = (F.mLyrics || '').trim() || '[Intro]\n\n[Chorus]\nOh oh oh, here we go\nFeel the rhythm, feel the flow';
        const blob = await mmGenMusic({ model: F.mModel, prompt: F.mPrompt, lyrics: ly,
          audio_setting: { sample_rate: 44100, bitrate: 256000, format: 'mp3' } });
        res = { kind: 'audio', sub: 'music', blob, url: URL.createObjectURL(blob) };
        onStatus && onStatus('Saving to storage…'); firstAsset = await saveOne(res);
      } else {
        const payload = { model: F.tModel, text: F.tText, stream: false,
          voice_setting: { voice_id: F.tVoice, speed: F.tSpeed, vol: F.tVol, pitch: F.tPitch },
          audio_setting: { sample_rate: 32000, bitrate: 128000, format: F.tFormat, channel: 1 } };
        if (F.tEmotion && F.tEmotion !== 'auto') payload.voice_setting.emotion = F.tEmotion;
        const blob = await mmGenVoice(payload);
        res = { kind: 'audio', sub: 'voice', fmt: F.tFormat, blob, url: URL.createObjectURL(blob) };
        onStatus && onStatus('Saving to storage…'); firstAsset = await saveOne(res);
      }
      setResult(res); setSaved(firstAsset);
      onStatus && onStatus('Generated & saved to storage ✓'); setTimeout(() => onStatus && onStatus(null), 2200);
    } catch (e) { setErr(String(e.message || e)); onStatus && onStatus(null); }
    setBusy(false);
  };

  const useIt = async () => { const a = saved || (result && await saveOne(result)); if (onUse && a) await onUse(a); onClose(); };
  const download = () => {
    if (!result) return;
    const a = document.createElement('a'); a.href = result.dataUrl || result.url;
    a.download = 'iraven-' + (result.sub || result.kind) + (result.kind === 'image' ? '.png' : result.kind === 'video' ? '.mp4' : (result.fmt === 'wav' ? '.wav' : '.mp3'));
    a.click();
  };

  const canSend = { image: F.iPrompt.trim(), video: F.vPrompt.trim(), music: F.mPrompt.trim(), voice: F.tText.trim() }[mtab];

  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 1400, background: 'rgba(3,5,14,0.82)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: BODY }}>
      <div onClick={e => e.stopPropagation()} style={{ width: 'min(580px, 95vw)', maxHeight: '90vh', overflowY: 'auto',
        borderRadius: 18, background: '#0c1222', border: '1px solid rgba(255,255,255,0.12)', padding: 22 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 14 }}>
          <span style={{ fontFamily: DISPLAY, fontWeight: 600, fontSize: 17, color: '#fff' }}>✦ AI media · MiniMax</span>
          <button style={btnStyle({ padding: '6px 12px' })} onClick={onClose}>✕</button>
        </div>

        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 6, marginBottom: 16 }}>
          {MM_TABS.map(t => (
            <button key={t.id} onClick={() => { setMtab(t.id); setResult(null); setSaved(null); setErr(null); }} style={{
              padding: '9px 6px', borderRadius: 10, cursor: 'pointer', fontFamily: BODY, fontSize: 13, fontWeight: 600,
              border: mtab === t.id ? '1px solid #2f6df6' : '1px solid rgba(255,255,255,0.10)',
              background: mtab === t.id ? 'rgba(47,109,246,0.18)' : 'rgba(255,255,255,0.03)',
              color: mtab === t.id ? '#9dbcff' : 'rgba(255,255,255,0.7)' }}>{t.label}</button>
          ))}
        </div>

        {mtab === 'image' ? (<>
          <MMRow label="Model"><MMSelect value={F.iModel} onChange={set('iModel')} options={MM_IMAGE_MODELS} /></MMRow>
          <Field label="Prompt" rows={3} value={F.iPrompt} onChange={set('iPrompt')}
            placeholder="Cinematic stadium under floodlights, dark blue mood, no text…" />
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
            <MMRow label="Aspect ratio"><MMSelect value={F.iAspect} onChange={set('iAspect')} options={MM_ASPECTS} /></MMRow>
            <MMRow label="Count" hint={F.iN + ' image' + (F.iN > 1 ? 's' : '')}>
              <MMSlider value={F.iN} min={1} max={9} step={1} onChange={set('iN')} /></MMRow>
          </div>
          <MMImageInput label="Character reference (optional)" hint="keep a face/subject consistent"
            asset={F.iSubject} assets={assets} setAssets={setAssets} onPick={set('iSubject')} />
          <MMRow label="Prompt optimizer">
            <Seg options={[{ id: true, label: 'On' }, { id: false, label: 'Off' }]} value={F.iOpt} onChange={set('iOpt')} /></MMRow>
        </>) : mtab === 'video' ? (<>
          <MMRow label="Model" hint={vModel.director ? '[pan] [zoom] [push in] cues OK' : ''}>
            <MMSelect value={F.vModel} onChange={set('vModel')} options={MM_VIDEO_MODELS} /></MMRow>
          <Field label="Prompt" rows={3} value={F.vPrompt} onChange={set('vPrompt')}
            placeholder={vModel.director ? '[push in] Slow dolly through a glowing stadium at night…' : 'Slow dolly through a glowing football stadium at night…'} />
          {(vModel.firstFrame || vModel.id.indexOf('I2V') === 0) ? (
            <MMImageInput label={vModel.lastFrame ? 'First frame (image → video)' : 'Source image (image → video)'} hint="optional"
              asset={F.vFirst} assets={assets} setAssets={setAssets} onPick={set('vFirst')} />
          ) : null}
          {vModel.lastFrame ? (
            <MMImageInput label="Last frame (optional)" hint="start → end morph" asset={F.vLast} assets={assets} setAssets={setAssets} onPick={set('vLast')} />
          ) : null}
          {vModel.subject ? (
            <MMImageInput label="Subject reference — face (required)" hint="keeps the person consistent"
              asset={F.vSubject} assets={assets} setAssets={setAssets} onPick={set('vSubject')} />
          ) : null}
          {(vModel.durations || vModel.resolutions) ? (
            <div style={{ display: 'grid', gridTemplateColumns: vModel.durations && vModel.resolutions ? '1fr 1fr' : '1fr', gap: 12 }}>
              {vModel.durations ? <MMRow label="Duration"><MMSelect value={F.vDur} onChange={v => set('vDur')(+v)}
                options={vModel.durations.map(d => ({ id: d, label: d + 's' }))} /></MMRow> : null}
              {vModel.resolutions ? <MMRow label="Resolution"><MMSelect value={F.vRes} onChange={set('vRes')}
                options={vModel.resolutions} /></MMRow> : null}
            </div>
          ) : null}
          <MMRow label="Prompt optimizer">
            <Seg options={[{ id: true, label: 'On' }, { id: false, label: 'Off' }]} value={F.vOpt} onChange={set('vOpt')} /></MMRow>
          {F.vRes === '1080P' && F.vDur === 10 ? (
            <p style={{ margin: '0 0 10px', fontSize: 11.5, color: '#ffcf8a', fontFamily: BODY }}>
              Note: 10s isn’t available at 1080P — switch to 768P or 6s.</p>
          ) : null}
          <p style={{ margin: '0 0 10px', fontSize: 11.5, color: 'rgba(255,255,255,0.4)', fontFamily: BODY }}>
            Video runs async on MiniMax — usually 1–5 minutes.</p>
        </>) : mtab === 'music' ? (<>
          <MMRow label="Model"><MMSelect value={F.mModel} onChange={set('mModel')} options={MM_MUSIC_MODELS} /></MMRow>
          <Field label="Style prompt" rows={2} value={F.mPrompt} onChange={set('mPrompt')}
            placeholder="Energetic electronic sports anthem, driving beat, 120bpm" />
          <Field label="Lyrics (required by MiniMax — auto-filled if empty)" rows={3} value={F.mLyrics} onChange={set('mLyrics')}
            placeholder={'[Intro]\n[Verse] …\n[Chorus] …'} />
        </>) : (<>
          <MMRow label="Model"><MMSelect value={F.tModel} onChange={set('tModel')} options={MM_VOICE_MODELS} /></MMRow>
          <Field label="Voiceover text" rows={3} value={F.tText} onChange={set('tText')}
            placeholder="Every match. Every angle. Download now." />
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
            <MMRow label="Voice"><MMSelect value={F.tVoice} onChange={set('tVoice')} options={MM_VOICES} /></MMRow>
            <MMRow label="Emotion"><MMSelect value={F.tEmotion} onChange={set('tEmotion')} options={MM_EMOTIONS} /></MMRow>
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 }}>
            <MMRow label="Speed" hint={F.tSpeed.toFixed(2) + '×'}><MMSlider value={F.tSpeed} min={0.5} max={2} step={0.05} onChange={set('tSpeed')} fmt={v => v.toFixed(2)} /></MMRow>
            <MMRow label="Volume" hint={F.tVol.toFixed(1)}><MMSlider value={F.tVol} min={0.1} max={2} step={0.1} onChange={set('tVol')} fmt={v => v.toFixed(1)} /></MMRow>
            <MMRow label="Pitch" hint={String(F.tPitch)}><MMSlider value={F.tPitch} min={-12} max={12} step={1} onChange={set('tPitch')} /></MMRow>
          </div>
          <MMRow label="Format"><MMSelect value={F.tFormat} onChange={set('tFormat')} options={MM_AUDIO_FORMATS} /></MMRow>
        </>)}

        <button disabled={!canSend || busy} onClick={run}
          style={btnStyle({ width: '100%', marginTop: 4, padding: '12px 0',
            background: 'linear-gradient(180deg,#3b78ff,#1f54d6)', border: '1px solid #3b78ff', opacity: !canSend || busy ? 0.55 : 1 })}>
          {busy ? 'Generating…' : 'Generate'}</button>

        {err ? (
          <div style={{ marginTop: 12, padding: '10px 14px', borderRadius: 12, fontSize: 13,
            background: 'rgba(255,59,70,0.12)', border: '1px solid rgba(255,59,70,0.35)', color: '#ff9ba1' }}>{err}</div>
        ) : null}

        {result ? (
          <div style={{ marginTop: 16 }}>
            {result.kind === 'image' ? (
              <img src={result.dataUrl} style={{ width: '100%', maxHeight: 320, objectFit: 'contain', borderRadius: 12, background: '#05070f' }} />
            ) : result.kind === 'video' ? (
              <video src={result.url} controls muted playsInline style={{ width: '100%', maxHeight: 320, borderRadius: 12, background: '#05070f' }} />
            ) : (
              <audio src={result.url} controls style={{ width: '100%' }} />
            )}
            <div style={{ display: 'flex', gap: 8, marginTop: 10, alignItems: 'center' }}>
              {saved ? <span style={{ fontSize: 11.5, color: '#7ee0a6', fontFamily: "'JetBrains Mono',monospace", marginRight: 'auto' }}>
                ✓ in storage{result.count > 1 ? ' (' + result.count + ')' : ''}</span> : null}
              {onUse ? <button style={btnStyle({ flex: saved ? '0 0 auto' : 1, background: 'linear-gradient(180deg,#22a45c,#157a41)', border: '1px solid #22a45c' })} onClick={useIt}>Use here</button> : null}
              <button style={btnStyle({ flex: (onUse || saved) ? '0 0 auto' : 1 })} onClick={onClose}>Done</button>
              <button style={btnStyle()} onClick={download}>Download</button>
            </div>
          </div>
        ) : null}
      </div>
    </div>
  );
}

Object.assign(window, { AIMediaModal, mmRequest, mmGenImage, mmGenVideo, mmGenMusic, mmGenVoice, assetToDataURL });
