/* ============ Genius — STUDIO CRÉATIF (20/09/2026) ============
   Depuis une campagne existante : A) trois directions créatives → B) script + storyboard + réglages + coût → C) génération (Higgsfield, côté serveur),
   résultats conservés, variantes. Aucune génération payante sans validation explicite. Les états affichés sont ceux du serveur (aucun pourcentage inventé).
   Tout le contexte vient de studio_brief (marque, offre, message, visuels, LP) — jamais la base CRM. */
const ST_STEPS = ['Direction créative', 'Préparation', 'Résultats'];   /* parcours image */
const SP_STEPS = ['Direction créative', 'Storyboard', 'Génération', 'Montage'];   /* parcours spot vidéo (Seedance 2.5) */
const ST_RATIOS_VIDEO = ['9:16', '16:9', '1:1', '4:3', '3:4'];
const ST_RATIOS_IMAGE = ['1:1', '4:3', '3:4', '16:9', '9:16', '3:2', '2:3'];
const stPost = (action, body) => fetch(GAPI() + '?action=' + action, { method: 'POST', headers: GHEAD(), body: JSON.stringify(Object.assign({ tenant: window.geniusTenant() }, body)) }).then(r => r.json());
const stKey = () => 'k' + Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
const ST_STATUS = { queued: ['En attente', 'wait'], in_progress: ['En cours', 'run'], completed: ['Terminé', 'ok'], failed: ['Erreur', 'err'], nsfw: ['Refusé (modération)', 'err'], canceled: ['Annulée', 'muted'], download_failed: ['Généré · rapatriement échoué', 'err'], submit_uncertain: ['Soumission incertaine', 'err'] };
const ST_PROVIDER = { higgsfield: 'Higgsfield', sora: 'OpenAI Sora 2 (ancien flux, retiré des nouvelles générations)' };
function stStatus(t) { const s = ST_STATUS[t.status] || [t.status || '—', 'muted']; return t.phase === 'downloading' ? ['Rapatriement', 'run'] : s; }
function stFmtCost(e) { if (!e) return ''; if (!e.known) return 'Coût inconnu' + (e.reason ? ' — ' + e.reason : ''); const pre = e.indicative ? 'Estimation indicative : ' : ''; const parts = []; if (e.credits != null) parts.push(Number(e.credits).toLocaleString('fr-FR', { maximumFractionDigits: 2 }) + ' crédits'); if (e.eur != null) parts.push('≈ ' + Number(e.eur).toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €'); else if (e.usd != null) parts.push(Number(e.usd).toFixed(2) + ' $'); return pre + (parts.join(' · ') || 'inconnu'); }
function stRatioAllowed(model, r) { const p = model && model.params && model.params.aspect_ratio; return !p || !p.enum || p.enum.includes(r); }
/* (audit C-A23) au changement de modèle, chaque réglage est ramené dans l'espace autorisé (enum / min / max / défaut) */
function stNormalize(s, m) { const p = m.params || {}; const n = { ...s, endpoint: m.endpoint };
  if (p.aspect_ratio) { if (!stRatioAllowed(m, n.aspect_ratio)) n.aspect_ratio = p.aspect_ratio.default || p.aspect_ratio.enum[0]; } else delete n.aspect_ratio;
  if (p.duration) { const d = Number(n.duration); if (p.duration.enum) { if (!p.duration.enum.includes(d)) n.duration = p.duration.default || p.duration.enum[0]; } else if (!(d >= p.duration.min && d <= p.duration.max)) n.duration = p.duration.default || p.duration.min; } else delete n.duration;
  ['resolution', 'quality', 'model'].forEach(k => { if (p[k] && p[k].enum && n[k] && !p[k].enum.includes(n[k])) n[k] = p[k].default || p[k].enum[0]; if (!p[k]) delete n[k]; });
  if (!p.image_url) n.image_url = ''; if (!p.image_urls) n.image_urls = []; if (!p.batch_size) delete n.batch_size; return n; }

function StField({ label, value, onChange, rows, placeholder, mono, type, autoComplete }) {
  return <label className="st-field"><span>{label}</span>{rows ? <textarea rows={rows} value={value || ''} placeholder={placeholder} onChange={e => onChange(e.target.value)} style={mono ? { fontFamily: 'var(--mono)', fontSize: 12.5 } : undefined} /> : <input type={type || 'text'} autoComplete={autoComplete} value={value || ''} placeholder={placeholder} onChange={e => onChange(e.target.value)} />}</label>;
}
function StModeBadge({ status }) {
  if (!status) return null;
  if (status.demo) return <span className="st-mode st-mode-test" title="Concepts, storyboard et médias simulés : aucun appel facturé.">Démonstration · rien n’est facturé</span>;
  if (status.mode === 'mock') return <span className="st-mode st-mode-test">Mode test global · médias simulés</span>;
  if (status.mode === 'live') return <span className="st-mode st-mode-live">Higgsfield connecté{status.auth && !status.auth.generation_validated ? ' · génération non encore validée' : ''}</span>;
  return <span className="st-mode st-mode-off" title={status.blocker || status.message || ''}>Higgsfield non connecté{status.missing && status.missing.length ? ' · manque : ' + status.missing.join(' + ') : ''}</span>;
}

/* ---------- Étape A : brief ajustable + 3 directions ---------- */
function StBrief({ brief, onChange, open, setOpen }) {
  const b = brief; const set = (path, v) => { const n = JSON.parse(JSON.stringify(b)); let o = n; const ps = path.split('.'); for (let i = 0; i < ps.length - 1; i++) o = o[ps[i]]; o[ps[ps.length - 1]] = v; onChange(n); };
  return <div className="card st-brief">
    <div className="st-brief-head" onClick={() => setOpen(!open)}>
      <div><div className="sec-title">Brief repris de la campagne</div><div className="sec-sub">{b.brand.name}{b.brand.sector ? ' · ' + b.brand.sector : ''} · {b.product.products.length ? b.product.products.join(', ') : 'offre'}{b.images.length ? ' · ' + b.images.length + ' visuel' + (b.images.length > 1 ? 's' : '') : ''}{b.lp && b.lp.url ? ' · page de destination' : ''}</div></div>
      <button type="button" className="lnk">{open ? 'Réduire' : 'Ajuster'}</button>
    </div>
    {open && <div className="st-brief-grid">
      <StField label="Accroche / message" value={b.message.hook} onChange={v => set('message.hook', v)} rows={2} />
      <StField label="Offre" value={b.product.offer} onChange={v => set('product.offer', v)} />
      <StField label="Urgence / date limite" value={b.product.urgency} onChange={v => set('product.urgency', v)} />
      <StField label="Objectif" value={b.product.objective} onChange={v => set('product.objective', v)} />
      <StField label="Audience" value={b.audience} onChange={v => set('audience', v)} rows={2} />
      <StField label="Appel à l’action" value={b.message.cta} onChange={v => set('message.cta', v)} />
      <div className="st-field"><span>Identité visuelle</span><div className="st-brand"><i style={{ background: b.brand.colors.primary || 'var(--line)' }} /><i style={{ background: b.brand.colors.accent || 'var(--line)' }} /><span>{b.brand.tone}{b.brand.font ? ' · ' + b.brand.font : ''}{b.brand.has_logo ? ' · logo disponible' : ''}</span></div></div>
      {b.images.length > 0 && <div className="st-field st-field-wide"><span>Visuels disponibles (références possibles)</span><div className="st-refs">{b.images.map((im, i) => <figure key={i}><img src={im.url} alt="" loading="lazy" /><figcaption>{im.label}</figcaption></figure>)}</div></div>}
    </div>}
  </div>;
}
function StConcepts({ concepts, chosen, onChoose, onEdit, busy, onMore }) {
  if (busy) return <CampaignGenerationLoader variant="compact" title="Genius explore trois directions créatives…" sub="Aucun média n’est généré à cette étape." />;
  if (!concepts || !concepts.length) return null;
  return <div className="st-concepts">{concepts.map(k => {
    const on = chosen && chosen.id === k.id; const c = on ? chosen : k;
    return <div key={k.id} className={'card st-concept' + (on ? ' on' : '')}>
      <div className="st-concept-k">Direction {k.id.toUpperCase()}{k.mood ? ' · ' + k.mood : ''}</div>
      {on ? <>
        <StField label="Titre" value={c.title} onChange={v => onEdit({ ...c, title: v })} />
        <StField label="Accroche" value={c.hook} onChange={v => onEdit({ ...c, hook: v })} />
        <StField label="Visuel" value={c.visual} onChange={v => onEdit({ ...c, visual: v })} rows={3} />
        <StField label="Scénario vidéo" value={c.scenario} onChange={v => onEdit({ ...c, scenario: v })} rows={3} />
        <StField label="Bénéfice" value={c.benefit} onChange={v => onEdit({ ...c, benefit: v })} />
      </> : <>
        <h4>{k.title}</h4><p className="st-hook">« {k.hook} »</p>
        <p><b>Visuel</b> — {k.visual}</p><p><b>Scénario</b> — {k.scenario}</p><p className="st-benefit"><Icon name="check" size={13} /> {k.benefit}</p>
      </>}
      <div className="st-concept-a">{on ? <span className="pill pill-sm">Direction retenue · modifiable</span> : <Btn size="sm" onClick={() => onChoose(k)}>Choisir cette direction</Btn>}</div>
    </div>;
  })}</div>;
}

/* ---------- Étape B : script, storyboard, réglages, coût ---------- */
function StStoryboard({ prep, onChange, kind }) {
  const sb = prep.storyboard || []; const setSb = a => onChange({ ...prep, storyboard: a });
  const total = sb.reduce((s, x) => s + (Number(x.seconds) || 0), 0);
  return <div className="card st-prep">
    <div className="sec-head" style={{ padding: 0, marginBottom: 12 }}><div><div className="sec-title">Script et storyboard</div><div className="sec-sub">Tout est modifiable. {kind === 'video' ? 'Durée des plans : ' + total + ' s.' : 'Pour une image, seul le prompt visuel compte ; le script sert aux légendes.'}</div></div></div>
    <div className="st-script">
      <StField label="Voix off" value={prep.script.voiceover} onChange={v => onChange({ ...prep, script: { ...prep.script, voiceover: v } })} rows={2} />
      <StField label="Textes à l’écran (un par ligne)" value={(prep.script.on_screen || []).join('\n')} onChange={v => onChange({ ...prep, script: { ...prep.script, on_screen: v.split('\n').map(x => x.trim()).filter(Boolean) } })} rows={3} />
    </div>
    {kind === 'video' && <div className="st-sb">{sb.map((s, i) => <div key={i} className="st-shot">
      <div className="st-shot-h"><span className="st-shot-n">{i + 1}</span><input className="st-shot-title" value={s.shot || ''} onChange={e => setSb(sb.map((x, j) => j === i ? { ...x, shot: e.target.value } : x))} placeholder="Titre du plan" /><label className="st-sec"><input type="number" min="1" max="30" value={s.seconds || 1} onChange={e => setSb(sb.map((x, j) => j === i ? { ...x, seconds: Math.max(1, Number(e.target.value) || 1) } : x))} /> s</label><button type="button" className="attr-x" title="Retirer ce plan" onClick={() => setSb(sb.filter((_, j) => j !== i))}>×</button></div>
      <textarea rows={2} value={s.action || ''} onChange={e => setSb(sb.map((x, j) => j === i ? { ...x, action: e.target.value } : x))} placeholder="Ce qui se passe" />
      <div className="st-shot-row"><input value={s.camera || ''} onChange={e => setSb(sb.map((x, j) => j === i ? { ...x, camera: e.target.value } : x))} placeholder="Cadrage / mouvement" /><input value={s.overlay || ''} onChange={e => setSb(sb.map((x, j) => j === i ? { ...x, overlay: e.target.value } : x))} placeholder="Texte à l’écran (facultatif)" /></div>
    </div>)}{sb.length < 4 && <button type="button" className="st-add" onClick={() => setSb([...sb, { n: sb.length + 1, seconds: 3, shot: '', action: '', camera: '', overlay: '' }])}><Icon name="plus" size={14} /> Ajouter un plan</button>}</div>}
    <details className="st-adv"><summary>Prompt média envoyé au générateur (anglais)</summary>
      <StField label={kind === 'video' ? 'Prompt vidéo' : 'Prompt image'} value={kind === 'video' ? prep.video_prompt : prep.image_prompt} onChange={v => onChange(kind === 'video' ? { ...prep, video_prompt: v } : { ...prep, image_prompt: v })} rows={5} mono />
      {prep.notes && <p className="sec-sub" style={{ margin: '6px 0 0' }}>{prep.notes}</p>}
    </details>
  </div>;
}
function StSettings({ status, settings, onChange, brief, estimate, onEstimate, estimating }) {
  const models = (status && status.models) || []; const kind = settings.kind;
  const avail = models.filter(m => m.kind === kind && (m.ops || []).includes(kind === 'image' ? 'image' : 'text-to-video'));
  const defEp = status && status.defaults && status.defaults[kind === 'image' ? 'image' : 'video'] ? status.defaults[kind === 'image' ? 'image' : 'video'].endpoint : '';   /* 20/09 : choix centralisé (aucun repli silencieux vers un autre modèle) */
  const model = avail.find(m => m.endpoint === settings.endpoint) || avail.find(m => m.endpoint === defEp) || avail[0];
  const providers = Array.from(new Set(avail.map(m => m.provider)));
  React.useEffect(() => { if (model && model.endpoint !== settings.endpoint) onChange({ ...settings, endpoint: model.endpoint }); }, [kind, model && model.endpoint]);
  const p = (model && model.params) || {};
  const ratios = (kind === 'video' ? ST_RATIOS_VIDEO : ST_RATIOS_IMAGE).filter(r => stRatioAllowed(model, r));
  const needsImage = !!(p.image_url && p.image_url.req); const refs = brief.images || [];
  const dmin = p.duration ? p.duration.min : 0, dmax = p.duration ? p.duration.max : 0;
  const set = (k, v) => onChange({ ...settings, [k]: v });
  return <div className="card st-settings">
    <div className="sec-head" style={{ padding: 0, marginBottom: 12 }}><div><div className="sec-title">Réglages</div><div className="sec-sub">Les options techniques sont dans « Avancé ».</div></div></div>
    <div className="st-set-row"><span>Type de média</span><div className="chips">{[['image', 'Image'], ['video', 'Vidéo']].map(([k, l]) => <button key={k} type="button" className={'chip' + (kind === k ? ' on' : '')} onClick={() => onChange({ ...settings, kind: k, endpoint: '' })}>{l}</button>)}</div></div>
    {!model && <div className="err-txt">Aucun modèle {kind === 'video' ? 'vidéo' : 'image'} disponible{status && status.mode === 'off' ? ' : Higgsfield n’est pas connecté (Réglages → Connexions).' : '.'}</div>}
    {model && <>
      {providers.length > 1 && <div className="st-set-row"><span>Fournisseur</span><div className="chips">{providers.map(pv => <button key={pv} type="button" className={'chip' + (model.provider === pv ? ' on' : '')} onClick={() => { const m2 = avail.find(x => x.provider === pv && x.available) || avail.find(x => x.provider === pv); if (m2) onChange(stNormalize(settings, m2)); }}>{ST_PROVIDER[pv] || pv}{!avail.some(x => x.provider === pv && x.available) ? ' · indisponible' : ''}</button>)}<span className="sec-sub">{status.providers && status.providers[model.provider] ? status.providers[model.provider].note : ''}</span></div></div>}
      {providers.length === 1 && kind === 'video' && <div className="st-set-row"><span>Fournisseur</span><span className="sec-sub">{ST_PROVIDER[model.provider] || model.provider} — {status.providers && status.providers.higgsfield && !status.providers.higgsfield.available ? 'Higgsfield ajouterait l’animation d’image et la retouche vidéo une fois connecté.' : ''}</span></div>}
      {ratios.length > 0 && <div className="st-set-row"><span>Format</span><div className="chips">{ratios.map(r => <button key={r} type="button" className={'chip' + (settings.aspect_ratio === r ? ' on' : '')} onClick={() => set('aspect_ratio', r)}>{r}{r === '9:16' ? ' · vertical' : r === '16:9' ? ' · paysage' : r === '1:1' ? ' · carré' : ''}</button>)}</div></div>}
      {!ratios.length && p.image_url && <div className="st-set-row"><span>Format</span><span className="sec-sub">Celui de l’image de départ.</span></div>}
      {p.duration && <div className="st-set-row"><span>Durée</span><div className="chips">{(p.duration.enum ? p.duration.enum : [4, 5, 8, 10, 12, 15, 20].filter(d => d >= dmin && d <= dmax)).map(d => <button key={d} type="button" className={'chip' + (Number(settings.duration) === d ? ' on' : '')} onClick={() => set('duration', d)}>{d} s</button>)}</div></div>}
      {p.model && <div className="st-set-row"><span>Qualité Sora</span><div className="chips">{p.model.enum.map(mv => <button key={mv} type="button" className={'chip' + ((settings.model || p.model.default) === mv ? ' on' : '')} onClick={() => set('model', mv)}>{mv === 'sora-2-pro' ? 'Sora 2 Pro (≈ 3× le prix)' : 'Sora 2'}</button>)}</div></div>}
      {(p.image_urls || p.image_url) && <div className="st-set-row"><span>{needsImage ? 'Image de départ' : 'Références'}</span>
        {refs.length === 0 ? <span className="sec-sub">{needsImage ? 'Aucun visuel dans la campagne : générez d’abord une image, ou choisissez « texte → vidéo ».' : 'Aucun visuel de campagne à donner en référence.'}</span>
          : <div className="st-refpick">{refs.map((im, i) => { const on = needsImage ? settings.image_url === im.url : (settings.image_urls || []).includes(im.url); return <button type="button" key={i} className={'st-ref' + (on ? ' on' : '')} title={im.label} onClick={() => needsImage ? set('image_url', on ? '' : im.url) : set('image_urls', on ? settings.image_urls.filter(u => u !== im.url) : [...(settings.image_urls || []), im.url])}><img src={im.url} alt="" />{on && <Icon name="check" size={12} w={3} />}</button>; })}</div>}
      </div>}
      <details className="st-adv"><summary>Avancé</summary>
        <div className="st-set-row"><span>Modèle</span><select value={model.endpoint} onChange={e => { const m2 = avail.find(x => x.endpoint === e.target.value); onChange(m2 ? stNormalize(settings, m2) : { ...settings, endpoint: e.target.value }); }}>{avail.map(m => <option key={m.endpoint} value={m.endpoint} disabled={!m.available}>{m.label}{m.available ? '' : ' — indisponible pour ce compte'}</option>)}</select><span className="sec-sub">{model.hint}</span></div>
        {p.resolution && <div className="st-set-row"><span>Résolution</span><div className="chips">{p.resolution.enum.map(r => <button key={r} type="button" className={'chip' + ((settings.resolution || p.resolution.default) === r ? ' on' : '')} onClick={() => set('resolution', r)}>{r}</button>)}</div></div>}
        {p.quality && <div className="st-set-row"><span>Qualité</span><div className="chips">{p.quality.enum.map(r => <button key={r} type="button" className={'chip' + ((settings.quality || p.quality.default) === r ? ' on' : '')} onClick={() => set('quality', r)}>{r}</button>)}</div></div>}
        {p.generate_audio && <div className="st-set-row"><span>Son</span><Toggle on={settings.generate_audio !== false} onChange={v => set('generate_audio', v)} label={settings.generate_audio !== false ? 'Ambiance sonore générée' : 'Sans son'} /></div>}
        {p.batch_size && <div className="st-set-row"><span>Nombre d’images</span><div className="chips">{p.batch_size.enum.map(n => <button key={n} type="button" className={'chip' + (Number(settings.batch_size || 1) === n ? ' on' : '')} onClick={() => set('batch_size', n)}>{n}</button>)}</div></div>}
        <div className="st-set-row"><span>Moteur de raisonnement</span><span className="sec-sub">{status && status.reasoner_detail ? 'OpenAI · ' + status.reasoner_detail.model + ' (' + status.reasoner_detail.price_in + ' $ / ' + status.reasoner_detail.price_out + ' $ par M tokens)' : '—'} — se change dans Réglages → Connexions.</span></div>
      </details>
      <div className="st-cost"><div><div className="st-cost-l">Coût estimé</div><div className={'st-cost-v' + (estimate && estimate.known ? '' : ' unknown')}>{estimating ? 'Estimation…' : (estimate ? stFmtCost(estimate) : 'Non estimé')}</div></div><Btn kind="ghost" size="sm" icon="refresh" onClick={onEstimate} disabled={estimating}>Estimer</Btn></div>
    </>}
  </div>;
}

/* ---------- Étape C : aperçu, versions, actions ---------- */
function StPreview({ task }) {
  if (!task) return <div className="st-empty"><Spark size={26} /><div>Aucun média pour l’instant.</div></div>;
  const m = (task.media || []).find(x => x.url);
  const [lbl, tone] = stStatus(task);
  if (task.status === 'completed' && m) {
    if (m.kind !== 'video') return <div className="st-view"><img src={m.url} alt="" /></div>;
    const ar = (task.params && task.params.aspect_ratio) || '9:16'; const [aw, ah] = ar.split(':').map(Number); const portrait = aw < ah; const w = portrait ? 324 : 520; const h = Math.round(w * (ah / aw));
    return <div className="st-view"><VideoPlayer src={m.url} poster={m.poster} width={w} height={h} /></div>;
  }
  if (task.status === 'queued' || task.status === 'in_progress' || task.phase === 'downloading') {
    const phase = task.phase === 'downloading' ? 'final' : task.status === 'in_progress' ? 'gen' : 'prep';
    const sub = 'Vous pouvez quitter cet écran : la tâche continue côté serveur.' + (task.provider === 'sora' && task.progress ? ' Avancement Sora : ' + task.progress + ' %.' : '');
    return <div className="st-view st-view-wait">{task.kind === 'video' ? <VideoGenerationLoader phase={phase} title={task.status === 'queued' ? 'En attente chez le générateur…' : task.phase === 'downloading' ? 'Rapatriement dans Genius…' : 'Génération vidéo en cours…'} sub={sub} /> : <ImageGenerationLoader title={task.status === 'queued' ? 'En attente chez le générateur…' : task.phase === 'downloading' ? 'Rapatriement dans Genius…' : 'Génération de l’image…'} sub={sub} />}</div>;
  }
  if (task.status === 'download_failed') return <div className="st-view st-view-err"><Icon name="alert" size={22} /><div><b>Généré chez {ST_PROVIDER[task.provider] || 'le fournisseur'}, mais le rapatriement a échoué</b><div>{task.error}</div><div>Référence conservée : {task.provider_request_id}{task.provider_outputs_at ? ' · sorties disponibles jusqu’au ' + new Date(new Date(task.provider_outputs_at).getTime() + 7 * 86400000).toLocaleDateString('fr-FR') : ''}. Le bouton « Réessayer le téléchargement » ne relance pas de génération.</div></div></div>;
  if (task.status === 'submit_uncertain') return <div className="st-view st-view-err"><Icon name="alert" size={22} /><div><b>Soumission incertaine</b><div>{task.error}</div><div>La génération a peut-être démarré (et sera facturée). Genius ne la répète pas. Vérifiez sur la console du fournisseur ; pour relancer, lancez volontairement une nouvelle génération.</div></div></div>;
  return <div className="st-view st-view-err"><Icon name="alert" size={22} /><div><b>{lbl}</b>{task.error ? <div>{task.error}</div> : null}</div></div>;
}
function StVersions({ tasks, current, onPick }) {
  if (!tasks.length) return null;
  return <div className="st-versions">{tasks.map((t, i) => { const m = (t.media || []).find(x => x.url); const [lbl, tone] = stStatus(t); return <button type="button" key={t.id} className={'st-ver' + (current && current.id === t.id ? ' on' : '')} onClick={() => onPick(t)} title={t.label + ' · ' + lbl}>
    {m ? <img src={m.kind === 'video' ? (m.poster || '') : m.url} alt="" /> : <span className={'st-ver-ph ' + tone}>{t.kind === 'video' ? <Icon name="bolt" size={14} /> : <Icon name="image" size={14} />}</span>}
    <span className={'st-ver-s ' + tone}>{lbl}</span><span className="st-ver-n">{tasks.length - i}</span>{t.mock && <span className="st-ver-m">test</span>}
  </button>; })}</div>;
}

/* Réglages → Connexions : Higgsfield (utilisateurs Groupe Verlaine). La clé est enregistrée côté serveur via studio_config et n'est jamais réaffichée. */
function StudioConnection() {
  const [c, setC] = React.useState(null); const [st, setSt] = React.useState(null); const [form, setForm] = React.useState({}); const [msg, setMsg] = React.useState(''); const [busy, setBusy] = React.useState(false);
  const load = (reprobe) => { stPost('studio_config', {}).then(j => { if (j && j.ok) setC(j); else setC({ ok: false, error: (j && j.error) || 'erreur' }); }).catch(() => setC({ ok: false, error: 'réseau' })); stPost('studio_status', reprobe ? { reprobe: 1 } : {}).then(j => { if (j && j.ok) setSt(j); }).catch(() => {}); };   /* (audit C-A26) sonde uniquement après une action */
  React.useEffect(() => { load(); }, []);
  if (!c) return <Skeleton rows={2} />;
  if (c.ok === false) return <div className="sec-sub">Réservé aux utilisateurs Groupe Verlaine ({c.error}).</div>;
  const save = set => { setBusy(true); stPost('studio_config', { set }).then(j => { if (j && j.ok) { setForm({}); setMsg('Enregistré.'); load(!!(set.key_id || set.key_secret || set.mode)); setTimeout(() => setMsg(''), 3000); } else setMsg((j && (j.message || j.error)) || 'erreur'); }).catch(() => setMsg('réseau')).finally(() => setBusy(false)); };   /* (audit C-A35) */
  const connected = c.mode === 'live';
  return <div className="ads-plat" style={{ borderColor: connected ? 'var(--accent-line)' : undefined }}>
    <div className="ads-plat-h"><div><div className="ads-plat-t">Higgsfield (Studio créatif)</div><div className="sec-sub">{connected ? <>Connecté · clé {c.key_id}{st && st.probed_at ? ' · modèles vérifiés le ' + new Date(st.probed_at).toLocaleString('fr-FR', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }) : ''}</> : c.mode === 'mock' ? 'Mode test global : tout le monde voit des médias simulés — à réserver aux bancs d’essai.' : 'Non connecté : concepts, storyboards et vidéo Sora fonctionnent ; les images, l’animation d’image et la retouche vidéo Higgsfield non.'}</div></div><StModeBadge status={st || { mode: c.mode }} /></div>
    {c.missing && c.missing.length > 0 && <div className="st-notice"><Icon name="alert" size={14} /> <span>Configuration incomplète — champ manquant : <b>{c.missing.join(' et ')}</b>. Les deux valeurs viennent de console.higgsfield.ai → API keys ; le secret se saisit ici uniquement, il n’est jamais réaffiché.</span></div>}
    {c.defaults && <div className="st-set-row"><span>Modèles par défaut</span><div className="sec-sub">Vidéo : <b>{c.defaults.video}</b> (Studio, pubs de campagne, réseaux sociaux) · Image : <b>{c.defaults.image}</b>{st && st.defaults ? <> — {Object.entries(st.defaults).map(([k, v]) => <span key={k} style={{ marginRight: 8 }}>{v.available === true ? '✓' : v.available === false ? '✕' : '?'} {v.label}</span>)}</> : null}<br />Sora n’est plus utilisé pour les nouvelles vidéos ; aucun repli silencieux vers un autre modèle.</div></div>}
    <div className="st-set-row"><span>Clé API</span><div className="ad-dates" style={{ gridTemplateColumns: '1fr 1fr auto' }}>
      <input value={form.key_id != null ? form.key_id : (c.key_id || '')} onChange={e => setForm(f => ({ ...f, key_id: e.target.value }))} placeholder="Key ID (console.higgsfield.ai → API keys)" />
      <input type="password" autoComplete="new-password" value={form.key_secret || ''} onChange={e => setForm(f => ({ ...f, key_secret: e.target.value }))} placeholder={c.has_secret ? 'Secret déjà enregistré' : 'Key secret'} />
      <Btn size="sm" onClick={() => save({ key_id: form.key_id != null ? form.key_id : 'GNSKEEP', key_secret: form.key_secret || 'GNSKEEP', mode: 'live' })} disabled={busy || (!form.key_secret && !c.has_secret)}>Connecter</Btn></div></div>
    <div className="st-set-row"><span>Mode</span><div className="chips">{[['live', 'Réel (clé requise)'], ['off', 'Désactivé'], ['mock', 'Test global (simulé)']].map(([k, l]) => <button key={k} type="button" className={'chip' + (c.mode === k ? ' on' : '')} disabled={busy || (k === 'live' && !c.has_secret)} onClick={() => save({ mode: k })}>{l}</button>)}</div></div>
    <div className="st-set-row"><span>État</span><div><div className="st-states">{[['Identifiants enregistrés', c.auth && c.auth.saved], ['Authentification vérifiée', c.auth && c.auth.verified], ['Estimation obtenue', !!(c.estimate_ok_at || (st && st.models && st.models.some(m => m.probe && m.probe.estimate)))], ['Génération réellement validée', !!c.generation_validated_at]].map(([l, ok], i) => <span key={i} className={'st-state' + (ok ? ' ok' : '')}><Icon name={ok ? 'check' : 'clock'} size={12} w={3} /> {l}</span>)}{c.auth && c.auth.error && <span className="err-txt">{c.auth.error}</span>}{c.auth && c.auth.saved && <Btn size="sm" kind="ghost" onClick={() => { setBusy(true); stPost('studio_config', { verify: 1 }).then(j => { if (j && j.ok) setC(j); load(true); }).catch(() => setMsg('réseau')).finally(() => setBusy(false)); }} disabled={busy}>Vérifier l’authentification</Btn>}</div><div className="sec-sub" style={{ marginTop: 6 }}>Une estimation obtenue ne prouve pas l’autorisation de générer ; « génération validée » se coche au premier média réel rapatrié.</div></div></div>
    <div className="st-set-row"><span>Raisonnement</span><div><div className="chips">{Object.entries(c.reasoners || {}).map(([k, r]) => <button key={k} type="button" className={'chip' + (c.reasoner === k ? ' on' : '')} disabled={busy} onClick={() => save({ reasoner: k })}>{r.label} · {r.price_in} $ / {r.price_out} $ par M tokens</button>)}</div><div className="sec-sub" style={{ marginTop: 6 }}>Concepts, script et storyboard. GPT-6 Astra : vérifié accessible à notre projet OpenAI (Chat Completions, reasoning_effort « {c.reasoning_effort || 'low'} »), 4 à 5 fois plus cher que GPT-4o. Le modèle actuel reste GPT-4o tant que vous ne changez pas ici.</div></div></div>
    {st && st.models && connected && <div className="st-set-row"><span>Modèles</span><div className="sec-sub">{st.models.map(m => <div key={m.endpoint}>{m.available ? '✓' : '✕'} {m.label}{m.probe && m.probe.estimate && m.probe.estimate.usd != null ? ' · test ≈ ' + m.probe.estimate.usd + ' $' : ''}{m.probe && m.probe.error ? ' — ' + m.probe.error : ''}</div>)}</div></div>}
    <div className="sec-sub" style={{ marginTop: 8 }}>Les utilisateurs Groupe Verlaine disposent dans le Studio d’un bouton « Démonstration » : concepts, storyboard et médias simulés, aucun appel facturé.</div>
    {msg && <div className="ok-txt" style={{ marginTop: 6 }}>{msg}</div>}
  </div>;
}
function CreativeStudio({ campaignId, onBack }) {
  const [status, setStatus] = React.useState(null); const [data, setData] = React.useState(null); const [err, setErr] = React.useState('');
  const [step, setStep] = React.useState(0); const [brief, setBrief] = React.useState(null); const [briefOpen, setBriefOpen] = React.useState(false);
  const [concepts, setConcepts] = React.useState(null); const [concept, setConcept] = React.useState(null); const [notes, setNotes] = React.useState('');
  const [prep, setPrep] = React.useState(null); const [settings, setSettings] = React.useState({ kind: 'video', endpoint: '', aspect_ratio: '9:16', duration: 8, image_urls: [], image_url: '' });
  /* 20/09/2026 : spot vidéo (Seedance 2.5 via Higgsfield) — choix simples + références, l'objet `spot` vient du serveur */
  const [spot, setSpot] = React.useState(null); const [spotSettings, setSpotSettings] = React.useState({ objective: 'leads', style: 'documentaire', aspect_ratio: '9:16', duration: 12 }); const [refs, setRefs] = React.useState([]); const [spots, setSpots] = React.useState([]);
  const [busy, setBusy] = React.useState(''); const [estimate, setEstimate] = React.useState(null); const [confirm, setConfirm] = React.useState(null); const [llmMeta, setLlmMeta] = React.useState(null);
  const [testMode, setTestMode] = React.useState(false);   /* démonstration complète (utilisateurs Groupe Verlaine) : concepts, storyboard ET médias simulés, aucun appel facturé */
  const T = () => (testMode ? { test: 'demo' } : {});
  React.useEffect(() => { stPost('studio_status', T()).then(setStatus).catch(() => {}); }, [testMode]);
  const [adsSt, setAdsSt] = React.useState(null); const [adDraft, setAdDraft] = React.useState(null);   /* 20/09 : préparation d'annonces Meta / Google depuis une création */
  React.useEffect(() => { if (window.adPost) window.adPost('ads_status', {}).then(j => { if (j && j.ok) setAdsSt(j); }).catch(() => {}); }, []);
  const prepareAd = p => { if (!cur) return; setBusy('ad'); setErr(''); window.adPost('ads_prepare', { platform: p, campaign_id: campaignId, task: cur.id, studio_concept: concept ? concept.title + ' — ' + concept.hook : '' }).then(j => { setBusy(''); if (j && j.ok) setAdDraft(j.draft); else setErr((j && (j.detail || j.error)) || 'préparation impossible'); }).catch(() => { setBusy(''); setErr('réseau'); }); };
  const [tasks, setTasks] = React.useState([]); const [cur, setCur] = React.useState(null); const [action, setAction] = React.useState(null); const [actText, setActText] = React.useState(''); const [toast, setToast] = React.useState('');
  const late = useLateNotice(busy === 'concepts' || busy === 'prep');
  const flash = m => { setToast(m); setTimeout(() => setToast(''), 3500); };
  /* chargement : statut fournisseur + brief + état sauvegardé + tâches (reprise) */
  const loadGen = React.useRef(0);
  React.useEffect(() => {
    const gen = ++loadGen.current; setData(null); setBrief(null); setConcepts(null); setConcept(null); setPrep(null); setTasks([]); setCur(null); setConfirm(null); setErr(''); pendingRef.current = {};   /* (audit C-A13) */
    stPost('studio_brief', { id: campaignId }).then(j => {
      if (gen !== loadGen.current) return;
      if (!j || !j.ok) { setErr((j && j.error) || 'chargement impossible'); return; }
      setData(j); const s = j.studio || {}; setBrief(s.brief || j.brief); setConcepts(s.concepts || null); setConcept(s.concept || null); setPrep(s.prep || null); if (s.settings) setSettings(x => ({ ...x, ...s.settings }));
      if (s.spot_settings) setSpotSettings(x => ({ ...x, ...s.spot_settings })); if (Array.isArray(s.refs)) setRefs(s.refs);
      setTasks((j.tasks || []).filter(t => !t.spot)); const first = (j.tasks || []).filter(t => !t.spot)[0] || null; setCur(first);
      setStep(typeof s.step === 'number' ? s.step : (j.tasks && j.tasks.length ? 2 : (s.prep ? 1 : 0)));
      stPost('studio_spots', { campaign_id: campaignId }).then(k => { if (gen !== loadGen.current || !k || !k.ok) return; setSpots(k.spots || []); const want = s.spot_id ? (k.spots || []).find(x => x.id === s.spot_id) : null; if (want) setSpot(want); }).catch(() => {});
    }).catch(() => setErr('réseau'));
  }, [campaignId]);
  /* persistance légère de l'état du studio (jamais les tâches, qui ont leur propre store) */
  const saveTimer = React.useRef(null); const pendingRef = React.useRef({});
  const persist = React.useCallback((patch) => { Object.assign(pendingRef.current, patch); clearTimeout(saveTimer.current); saveTimer.current = setTimeout(() => { const p = pendingRef.current; pendingRef.current = {}; stPost('studio_save', { campaign_id: campaignId, studio: p }).catch(() => {}); }, 600); }, [campaignId]);   /* (audit C-A3) union des patches */
  React.useEffect(() => { if (data) persist({ step }); }, [step]);
  React.useEffect(() => { if (data && brief) persist({ brief }); }, [brief]);
  React.useEffect(() => { if (data && concept) persist({ concept }); }, [concept]);
  React.useEffect(() => { if (data && prep) persist({ prep }); }, [prep]);
  React.useEffect(() => { if (data) persist({ settings }); }, [settings]);
  React.useEffect(() => { if (data) persist({ spot_settings: spotSettings, refs }); }, [spotSettings, refs]);
  React.useEffect(() => { if (data && spot) persist({ spot_id: spot.id }); }, [spot && spot.id]);
  const runSpotPlan = () => { if (!concept) return; setBusy('prep'); setErr(''); window.geniusLong('studio_spot_plan', { ...T(), campaign_id: campaignId, brief, concept, settings: spotSettings, refs, notes }).then(j => { setBusy(''); if (j && j.ok) { setSpot(j.spot); setSpots(x => [j.spot, ...x]); setLlmMeta(j.llm || null); setStep(1); if (j.blocker && !testMode) setErr(j.blocker); } else setErr((j && (j.detail || j.error)) || 'échec'); }).catch(e => { setBusy(''); setErr(e.message || 'réseau'); }); };
  /* suivi des tâches non terminées : interrogation avec recul progressif (2 s → 8 s) */
  React.useEffect(() => {
    const TERMINAL = ['completed', 'failed', 'nsfw', 'canceled', 'download_failed', 'submit_uncertain'];
    const pending = tasks.filter(t => !TERMINAL.includes(t.status)); if (!pending.length) return;
    let delay = 2000, stop = false, timer = null;
    const tick = async () => { for (const t of pending) { try { const j = await stPost('studio_task', { task: t.id }); if (j && j.ok) { setTasks(ts => ts.map(x => x.id === t.id ? j.task : x)); setCur(c => c && c.id === t.id ? j.task : c); if (j.task.status === 'completed') flash('Média prêt et conservé dans Genius.'); } } catch (e) {} } if (!stop) { delay = Math.min(8000, delay * 1.4); timer = setTimeout(tick, delay); } };
    timer = setTimeout(tick, delay); return () => { stop = true; clearTimeout(timer); };
  }, [tasks.map(t => t.id + ':' + t.status).join('|')]);

  const runConcepts = () => { setBusy('concepts'); setErr(''); window.geniusLong('studio_concepts', { ...T(), brief, notes }).then(j => { setBusy(''); if (j && j.ok) { setConcepts(j.concepts); persist({ concepts: j.concepts }); setLlmMeta(j.llm || null); } else setErr((j && (j.detail || j.error)) || 'échec'); }).catch(() => { setBusy(''); setErr('réseau'); }); };
  const runPrep = () => { if (!concept) return; setBusy('prep'); setErr(''); window.geniusLong('studio_prepare', { ...T(), brief, concept, kind: settings.kind, duration: settings.duration, aspect_ratio: settings.aspect_ratio, notes }).then(j => { setBusy(''); if (j && j.ok) { setPrep(j.prep); setStep(1); setLlmMeta(j.llm || null); } else setErr((j && (j.detail || j.error)) || 'échec'); }).catch(() => { setBusy(''); setErr('réseau'); }); };
  /* paramètres exacts envoyés au fournisseur — construits à partir des réglages, validés côté serveur */
  const buildParams = (over) => {
    const o = Object.assign({}, settings, over || {}); const isV = o.kind === 'video'; const p = { prompt: isV ? (o.prompt || (prep && prep.video_prompt) || '') : (o.prompt || (prep && prep.image_prompt) || '') };
    const m = (status && status.models || []).find(x => x.endpoint === o.endpoint); const sp = (m && m.params) || {};
    if (sp.aspect_ratio && o.aspect_ratio) p.aspect_ratio = o.aspect_ratio; if (sp.duration && o.duration) p.duration = Number(o.duration); if (sp.resolution && o.resolution) p.resolution = o.resolution; if (sp.quality && o.quality) p.quality = o.quality;
    if (sp.generate_audio && o.generate_audio === false) p.generate_audio = false; if (sp.batch_size && o.batch_size) p.batch_size = Number(o.batch_size); if (sp.model && o.model) p.model = o.model;
    if (sp.image_url && o.image_url) p.image_url = o.image_url; if (sp.image_urls && (o.image_urls || []).length) p.image_urls = o.image_urls; if (sp.video_url && o.video_url) p.video_url = o.video_url;
    return p;
  };
  const runEstimate = () => { setBusy('estimate'); stPost('studio_estimate', { ...T(), endpoint: settings.endpoint, params: buildParams() }).then(j => { setBusy(''); setEstimate(j && j.ok ? j : { known: false, reason: (j && j.error) || 'erreur' }); }).catch(() => { setBusy(''); setEstimate({ known: false, reason: 'réseau' }); }); };
  const askGenerate = (opts) => { const o = opts || {}; const base = (o.variant_of && cur && cur.id === o.variant_of && cur.params) ? { ...cur.params, endpoint: cur.endpoint, kind: cur.kind } : {}; const params = buildParams({ ...base, ...(o.over || {}) }); const endpoint = (o.over && o.over.endpoint) || base.endpoint || settings.endpoint;   /* (audit C-A14) une variante part de la version sélectionnée */ setConfirm({ endpoint, params, label: o.label || (settings.kind === 'video' ? 'Vidéo' : 'Image') + (concept ? ' · ' + concept.title : ''), variant_of: o.variant_of || null, note: o.note || '', estimate: null, loading: true });
    const sig = endpoint + '|' + JSON.stringify(params);
    stPost('studio_estimate', { ...T(), endpoint, params }).then(j => setConfirm(c => (c && (c.endpoint + '|' + JSON.stringify(c.params)) === sig) ? { ...c, estimate: j && j.ok ? j : { known: false, reason: (j && j.error) || 'erreur' }, loading: false } : c)).catch(() => setConfirm(c => (c && (c.endpoint + '|' + JSON.stringify(c.params)) === sig) ? { ...c, estimate: { known: false, reason: 'réseau' }, loading: false } : c)); };   /* (audit C-A24) */
  const doGenerate = () => { const c = confirm; if (!c) return; setBusy('generate'); setConfirm(null);
    stPost('studio_generate', { ...T(), campaign_id: campaignId, endpoint: c.endpoint, params: c.params, confirmed: true, client_key: stKey(), label: c.label, variant_of: c.variant_of, note: c.note, concept: concept ? { id: concept.id, title: concept.title } : null }).then(j => { setBusy(''); if (j && j.ok) { setTasks(ts => [j.task, ...ts.filter(x => x.id !== j.task.id)]); setCur(j.task); setStep(2); setAction(null); } else setErr((j && (j.message || j.error)) || 'échec du lancement'); }).catch(() => { setBusy(''); setErr('réseau'); }); };
  const retryDownload = t => { setBusy('retry'); stPost('studio_retry_download', { task: t.id }).then(j => { setBusy(''); if (j && j.task) { setTasks(ts => ts.map(x => x.id === t.id ? j.task : x)); setCur(j.task); } flash(j && j.ok ? 'Média rapatrié.' : ((j && j.error) || 'échec du téléchargement')); }).catch(() => { setBusy(''); flash('réseau'); }); };
  const cancelTask = t => stPost('studio_cancel', { task: t.id }).then(j => { if (j && j.ok) { setTasks(ts => ts.map(x => x.id === t.id ? j.task : x)); setCur(j.task); } else flash((j && j.error) || 'annulation impossible'); });
  const attach = (t, asMain) => stPost('studio_attach', { task: t.id, media_index: 0, as_main: asMain }).then(j => { if (j && j.ok) flash(asMain ? 'Média associé et défini comme visuel principal de la campagne.' : 'Média associé à la campagne.'); else flash((j && j.error) || 'échec'); });
  const removeTask = t => { if (!window.confirm('Supprimer cette version et son média ?')) return; stPost('studio_task_remove', { task: t.id }).then(j => { if (j && j.ok) { setTasks(ts => { const n = ts.filter(x => x.id !== t.id); setCur(n[0] || null); return n; }); setData(d => d && d.studio ? { ...d, studio: { ...d.studio, attached: (d.studio.attached || []).filter(a => a.task !== t.id) } } : d); } else flash((j && j.error) || 'suppression impossible'); }); };
  const download = t => { const m = (t.media || []).find(x => x.url); if (!m) return; if (m.kind === 'image' && window.soDownload) { window.soDownload([m.url], 'studio-' + (t.label || 'visuel').toLowerCase().replace(/[^a-z0-9]+/gi, '-').slice(0, 40)).catch(e => flash(e.message || 'téléchargement impossible')); } else window.open(m.url, '_blank', 'noopener'); };

  if (err && !data) return <Card><div className="err-txt">Studio non chargé : {err}</div><div style={{ marginTop: 10 }}><Btn kind="ghost" size="sm" onClick={onBack}>← Retour</Btn></div></Card>;
  if (!data || !brief) return <Card><Skeleton rows={4} /></Card>;
  const models = (status && status.models) || []; const model = models.find(m => m.endpoint === settings.endpoint);
  const canGenerate = status && model && (model.available || testMode) && prep;
  const curMedia = cur && (cur.media || []).find(x => x.url);
  const editModel = models.find(m => m.endpoint === 'bytedance/seedance-2.5/video-edit'); const i2v = models.find(m => m.kind === 'video' && m.params && m.params.image_url && m.available);
  return <div className="studio">
    <div className="list-head" style={{ alignItems: 'center' }}>
      <div><button className="lnk lnk-muted" onClick={onBack}>← {data.campaign.title || 'Campagne'}</button><h3 style={{ fontSize: 22, marginTop: 6 }}>Studio créatif</h3><div className="list-sub">Images, vidéos et variantes à partir de la campagne. <StModeBadge status={status} /></div></div>
      <div className="head-actions"><Stepper steps={settings.kind === 'video' ? SP_STEPS : ST_STEPS} current={step} onJump={i => { if (settings.kind === 'video' && i > 0 && !spot) return; setStep(i); }} /></div>
    </div>
    {status && status.mode === 'off' && !testMode && <div className="st-notice" style={{ alignItems: 'center' }}><Icon name="alert" size={15} /> <span style={{ flex: 1 }}>{status.blocker || 'Higgsfield n’est pas connecté.'} Concepts et storyboard fonctionnent ; aucune génération d’image ou de vidéo n’est possible tant que la connexion n’est pas complète (aucun autre fournisseur n’est substitué).</span>{status.test_available && <Btn size="sm" kind="ghost" onClick={() => setTestMode(true)}>Démonstration (rien de facturé)</Btn>}<Btn size="sm" onClick={() => window.geniusGo && window.geniusGo('config')} icon="link">Connecter Higgsfield</Btn></div>}
    {testMode && <div className="st-notice" style={{ alignItems: 'center' }}><Icon name="alert" size={15} /> <span style={{ flex: 1 }}>Démonstration : concepts, storyboard et médias sont SIMULÉS — aucun appel OpenAI ni Higgsfield ni Sora, rien n’est facturé.</span><Btn size="sm" kind="ghost" onClick={() => setTestMode(false)}>Quitter la démonstration</Btn></div>}
    {err && <div className="err-txt" style={{ marginBottom: 12 }}>{err}</div>}
    {toast && <div className="st-toast">{toast}</div>}

    {step === 0 && <>
      <StBrief brief={brief} onChange={setBrief} open={briefOpen} setOpen={setBriefOpen} />
      <div className="card st-ask"><div><div className="sec-title">Direction créative</div><div className="sec-sub">Genius propose trois directions réellement différentes. Rien n’est généré : vous choisissez, puis vous ajustez.</div></div>
        <div className="st-ask-r"><input value={notes} onChange={e => setNotes(e.target.value)} placeholder="Consigne facultative (ex. « ambiance hiver, famille, pas de chantier »)" /><Btn onClick={runConcepts} disabled={!!busy} icon="bolt">{concepts ? 'Autres directions' : 'Proposer 3 directions'}</Btn></div></div>
      {late && busy === 'concepts' && <div className="sec-sub">Toujours en cours…</div>}
      {!testMode && status && status.reasoner_detail && <div className="sec-sub">Concepts et storyboard : appel OpenAI {status.reasoner_detail.model} (facturé, généralement quelques centimes){llmMeta ? ' — dernier appel : ' + llmMeta.model + ', ≈ ' + llmMeta.cost_usd + ' $' : ''}.</div>}
      <StConcepts concepts={concepts} chosen={concept} onChoose={k => setConcept(k)} onEdit={setConcept} busy={busy === 'concepts'} />
      {concept && settings.kind === 'video' && <SpChoices settings={spotSettings} onChange={setSpotSettings} brief={brief} refs={refs} setRefs={setRefs} disabled={busy === 'prep'} />}
      {concept && <div className="st-next"><div className="chips"><span className="sec-sub">Préparer pour :</span>{[['video', 'un spot vidéo (Seedance 2.5)'], ['image', 'une image']].map(([k, l]) => <button key={k} type="button" className={'chip' + (settings.kind === k ? ' on' : '')} onClick={() => setSettings(s => ({ ...s, kind: k, endpoint: '', aspect_ratio: k === 'video' ? '9:16' : '4:3' }))}>{l}</button>)}</div><Btn onClick={settings.kind === 'video' ? runSpotPlan : runPrep} disabled={busy === 'prep'} iconRight="arrow">{busy === 'prep' ? 'Préparation…' : (settings.kind === 'video' ? 'Préparer le storyboard (Astra)' : 'Préparer le script et le storyboard')}</Btn></div>}
      {settings.kind === 'video' && spots.length > 0 && !busy && <div className="card"><div className="sec-title" style={{ marginBottom: 6 }}>Spots de cette campagne</div><div className="sp-list">{spots.map(x => <button type="button" key={x.id} className={'sp-item' + (spot && spot.id === x.id ? ' on' : '')} onClick={() => { setSpot(x); setStep(x.render && x.render.status === 'done' ? 3 : (x.gen_status === 'draft' ? 1 : 2)); }}>{x.render && x.render.poster ? <img src={x.render.poster} alt="" /> : <span className="st-ver-ph muted"><Icon name="bolt" size={14} /></span>}<span><b>{x.title || 'Spot'}</b><small>{new Date(x.created_at).toLocaleString('fr-FR', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })} · {x.settings.aspect_ratio} · {x.scenes.length} plans · {x.render && x.render.status === 'done' ? 'monté' : x.gen_status}{x.mock ? ' · test' : ''}</small></span></button>)}</div></div>}
      {busy === 'prep' && <CampaignGenerationLoader variant="compact" title="Genius écrit le script et le storyboard…" sub="Aucun média n’est généré à cette étape." />}
    </>}

    {settings.kind === 'video' && step > 0 && (spot ? <SpotStudio campaignId={campaignId} spot={spot} setSpot={setSpot} status={status} testMode={testMode} T={T} step={step} setStep={setStep} flash={flash} onBack={() => setStep(0)} /> : <div className="card"><EmptyState title="Pas encore de storyboard" sub="Choisissez une direction créative, puis « Préparer le storyboard »." action={<Btn size="sm" kind="ghost" onClick={() => setStep(0)}>← Direction créative</Btn>} /></div>)}
    {settings.kind === 'image' && step === 1 && <>
      {!prep ? <div className="card"><EmptyState title="Pas encore de préparation" sub="Choisissez d’abord une direction créative." action={<Btn size="sm" kind="ghost" onClick={() => setStep(0)}>← Direction créative</Btn>} /></div> : <div className="st-b">
        <StStoryboard prep={prep} onChange={setPrep} kind={settings.kind} />
        <div className="st-b-side">
          <StSettings status={status} settings={settings} onChange={s => { setSettings(s); setEstimate(null); }} brief={brief} estimate={estimate} onEstimate={runEstimate} estimating={busy === 'estimate'} />
          <div className="card st-launch">
            <div className="sec-sub">{testMode || (status && status.mode === 'mock') ? 'Démonstration : la génération sera simulée (aucun coût).' : 'La génération est payante chez ' + (model ? (ST_PROVIDER[model.provider] || 'le fournisseur') : 'le fournisseur') + ' : vous validez d’abord le fournisseur et le coût estimé.'}</div>
            <Btn full onClick={() => askGenerate()} disabled={!canGenerate || busy === 'generate'} icon="bolt">{busy === 'generate' ? 'Lancement…' : 'Lancer la génération'}</Btn>
            {!canGenerate && prep && <div className="sec-sub" style={{ marginTop: 6 }}>{(!model || !model.available) ? ((status && status.blocker) || 'Ce modèle n’est pas disponible pour ce compte Higgsfield (aucun autre modèle n’est substitué).') : ''}</div>}
            <button type="button" className="lnk lnk-muted" style={{ marginTop: 8 }} onClick={runPrep} disabled={busy === 'prep'}>Réécrire le script avec Genius</button>
          </div>
        </div>
      </div>}
    </>}

    {settings.kind === 'image' && step === 2 && <div className="st-c">
      <div className="st-c-main">
        <div className="card st-stage">
          <StPreview task={cur} />
          {cur && <div className="st-stage-meta"><div><b>{cur.label}</b><span className="sec-sub"> · {cur.kind === 'video' ? 'vidéo' : 'image'}{cur.params && cur.params.aspect_ratio ? ' · ' + cur.params.aspect_ratio : ''}{cur.params && cur.params.duration ? ' · ' + cur.params.duration + ' s' : ''}{cur.variant_of ? ' · variante' : ''}{cur.estimate && cur.estimate.usd != null ? ' · ' + stFmtCost({ known: true, credits: cur.estimate.credits, usd: cur.estimate.usd }) : ''}</span></div>
            <div className="st-stage-a">{(cur.status === 'queued') && cur.provider !== 'sora' && <Btn size="sm" kind="ghost" onClick={() => cancelTask(cur)}>Annuler</Btn>}{cur.status === 'download_failed' && <Btn size="sm" icon="refresh" onClick={() => retryDownload(cur)} disabled={busy === 'retry'}>{busy === 'retry' ? 'Téléchargement…' : 'Réessayer le téléchargement'}</Btn>}{['completed', 'failed', 'nsfw', 'canceled', 'download_failed', 'submit_uncertain'].includes(cur.status) && <button type="button" className="attr-x" title="Supprimer cette version" onClick={() => removeTask(cur)}>×</button>}{cur.status === 'completed' && curMedia && <><Btn size="sm" kind="ghost" icon="download" onClick={() => download(cur)}>Télécharger</Btn><Btn size="sm" icon="link" onClick={() => attach(cur, window.confirm('Définir ce média comme visuel principal de la campagne ?\n\nOK : il remplace le visuel actuel (l’ancien est conservé).\nAnnuler : il est seulement associé à la campagne.'))}>Associer à la campagne</Btn></>}</div></div>}
        </div>
        <StVersions tasks={tasks} current={cur} onPick={setCur} />
      </div>
      <div className="st-c-side">
        <div className="card st-actions">
          <div className="sec-title" style={{ marginBottom: 4 }}>Améliorer</div><div className="sec-sub" style={{ marginBottom: 10 }}>Chaque action relance une génération complète (le fournisseur ne retouche pas une image existante) — l’original est conservé.</div>
          <button type="button" className={'st-act' + (action === 'hook' ? ' on' : '')} onClick={() => { setAction(action === 'hook' ? null : 'hook'); setActText(concept ? concept.hook : ''); }}><Icon name="doc" size={15} /><span><b>Modifier l’accroche</b><small>nouvelle génération</small></span></button>
          <button type="button" className={'st-act' + (action === 'variant' ? ' on' : '')} onClick={() => { setAction(action === 'variant' ? null : 'variant'); setActText(''); }}><Icon name="refresh" size={15} /><span><b>Générer une variante</b><small>même direction, nouvelle proposition</small></span></button>
          <button type="button" className={'st-act' + (action === 'format' ? ' on' : '')} onClick={() => setAction(action === 'format' ? null : 'format')}><Icon name="grid" size={15} /><span><b>Adapter le format</b><small>autre ratio · nouvelle génération</small></span></button>
          {settings.kind === 'video' || (cur && cur.kind === 'video') ? <button type="button" className={'st-act' + (action === 'scene' ? ' on' : '')} disabled={!(editModel && editModel.available && cur && cur.kind === 'video' && cur.status === 'completed')} onClick={() => { setAction(action === 'scene' ? null : 'scene'); setActText(''); }}><Icon name="image" size={15} /><span><b>Modifier une scène</b><small>{editModel && editModel.available ? 'retouche de la vidéo existante (Seedance video-edit)' : 'indisponible pour ce compte'}</small></span></button> : null}
          {cur && cur.kind === 'image' && cur.status === 'completed' && i2v && <button type="button" className={'st-act' + (action === 'animate' ? ' on' : '')} onClick={() => { setAction(action === 'animate' ? null : 'animate'); setActText(prep ? prep.video_prompt : ''); }}><Icon name="bolt" size={15} /><span><b>Animer cette image</b><small>image → vidéo · nouvelle génération</small></span></button>}
          {action === 'hook' && <div className="st-act-box"><StField label="Nouvelle accroche" value={actText} onChange={setActText} rows={2} /><Btn size="sm" onClick={() => { const k = { ...(concept || {}), hook: actText }; setConcept(k); askGenerate({ over: { prompt: ((settings.kind === 'video' ? prep.video_prompt : prep.image_prompt) || '') + ' Headline mood: ' + actText }, label: (settings.kind === 'video' ? 'Vidéo' : 'Image') + ' · accroche modifiée', variant_of: cur ? cur.id : null, note: actText }); }}>Relancer avec cette accroche</Btn></div>}
          {action === 'variant' && <div className="st-act-box"><StField label="Ce qui doit changer (facultatif)" value={actText} onChange={setActText} rows={2} placeholder="ex. lumière du soir, autre famille, plan plus serré" /><Btn size="sm" onClick={() => askGenerate({ over: { prompt: ((settings.kind === 'video' ? prep.video_prompt : prep.image_prompt) || '') + (actText ? ' Variation: ' + actText : ' Alternative take, different composition.') }, label: (settings.kind === 'video' ? 'Vidéo' : 'Image') + ' · variante', variant_of: cur ? cur.id : null, note: actText })}>Générer la variante</Btn></div>}
          {action === 'format' && <div className="st-act-box"><div className="chips">{(settings.kind === 'video' ? ST_RATIOS_VIDEO : ST_RATIOS_IMAGE).filter(r => stRatioAllowed(model, r)).map(r => <button key={r} type="button" className={'chip' + (settings.aspect_ratio === r ? ' on' : '')} onClick={() => askGenerate({ over: { aspect_ratio: r }, label: (settings.kind === 'video' ? 'Vidéo' : 'Image') + ' · ' + r, variant_of: cur ? cur.id : null })}>{r}</button>)}</div></div>}
          {action === 'scene' && cur && curMedia && <div className="st-act-box"><StField label="Modification demandée (une scène, un élément)" value={actText} onChange={setActText} rows={2} placeholder="ex. remplacer le plan 2 par une vue du jardin au coucher du soleil" /><Btn size="sm" onClick={() => askGenerate({ over: { endpoint: 'bytedance/seedance-2.5/video-edit', kind: 'video', prompt: actText, video_url: curMedia.url, aspect_ratio: '', duration: 0 }, label: 'Vidéo · scène modifiée', variant_of: cur.id, note: actText })} disabled={!actText.trim()}>Retoucher la vidéo</Btn></div>}
          {action === 'animate' && cur && curMedia && i2v && <div className="st-act-box"><StField label="Mouvement souhaité (anglais accepté)" value={actText} onChange={setActText} rows={2} /><Btn size="sm" onClick={() => askGenerate({ over: { endpoint: i2v.endpoint, kind: 'video', prompt: actText, image_url: curMedia.url, aspect_ratio: '', duration: settings.duration || 5 }, label: 'Vidéo · depuis l’image', variant_of: cur.id, note: actText })}>Animer</Btn></div>}
          <button type="button" className="lnk lnk-muted" style={{ marginTop: 10 }} onClick={() => setStep(1)}>← Revenir à la préparation</button>
        </div>
        {cur && cur.status === 'completed' && curMedia && <div className="card st-actions">
          <div className="sec-title" style={{ marginBottom: 4 }}>Diffuser en publicité</div><div className="sec-sub" style={{ marginBottom: 10 }}>Prépare un brouillon d’annonce dans Genius avec les champs propres à chaque plateforme. Rien n’est publié sans votre validation.</div>
          <button type="button" className="st-act" disabled={busy === 'ad'} onClick={() => prepareAd('meta')}><Icon name="send" size={15} /><span><b>Préparer pour Meta Ads</b><small>fil 1:1 / 4:5 · stories 9:16 · texte, titre, bouton</small></span></button>
          <button type="button" className="st-act" disabled={busy === 'ad'} onClick={() => prepareAd('google')}><Icon name="send" size={15} /><span><b>Préparer pour Google Ads</b><small>Demand Gen / Performance Max (assets) + Search responsive</small></span></button>
          {adsSt && adsSt.mode === 'off' && <div className="sec-sub" style={{ marginTop: 6 }}>Applications publicitaires non configurées : le brouillon reste dans Genius.</div>}
          {busy === 'ad' && <div className="sec-sub" style={{ marginTop: 8 }}><span className="gspin" /> Genius adapte les textes à la plateforme…</div>}
        </div>}
        {(data.studio && data.studio.attached || []).length > 0 && <div className="card"><div className="sec-title" style={{ marginBottom: 6 }}>Associés à la campagne</div>{(data.studio.attached).map((a, i) => <div key={i} className="st-att"><img src={a.kind === 'video' ? (a.poster || '') : a.url} alt="" /><span>{a.label}</span></div>)}</div>}
      </div>
    </div>}

    {adDraft && adsSt && <AdDraftEditor campaignId={campaignId} draft={adDraft} specs={adsSt.specs[adDraft.platform]} status={adsSt} onClose={() => setAdDraft(null)} onPublished={d => setAdDraft(d)} />}
    {confirm && <div className="st-modal-bg" onClick={() => setConfirm(null)}><div className="st-modal" onClick={e => e.stopPropagation()}>
      <div className="sec-title">Lancer cette génération ?</div>
      <div className="st-modal-rows"><div><span>Média</span><b>{confirm.label}</b></div><div><span>Fournisseur appelé</span><b>{testMode ? 'aucun (démonstration)' : (ST_PROVIDER[(models.find(m => m.endpoint === confirm.endpoint) || {}).provider] || 'Higgsfield')}</b></div><div><span>Modèle</span><b>{(models.find(m => m.endpoint === confirm.endpoint) || {}).label || confirm.endpoint}</b></div>{confirm.params.aspect_ratio && <div><span>Format</span><b>{confirm.params.aspect_ratio}</b></div>}{confirm.params.duration && <div><span>Durée</span><b>{confirm.params.duration} s</b></div>}<div><span>Coût</span><b className={confirm.estimate && confirm.estimate.known ? '' : 'unknown'}>{confirm.loading ? 'Estimation…' : stFmtCost(confirm.estimate)}</b></div></div>
      {(testMode || (status && status.mode === 'mock')) && <div className="st-notice" style={{ margin: '10px 0 0' }}><Icon name="alert" size={14} /> <span>Démonstration : résultat simulé, aucun appel au fournisseur, aucun coût.</span></div>}
      {!testMode && confirm.estimate && confirm.estimate.known && <div className="sec-sub" style={{ marginTop: 8 }}>Le montant facturé est celui du fournisseur pour la requête réelle ; l’estimation n’est pas un prix garanti.</div>}
      {status && status.mode === 'live' && confirm.estimate && !confirm.estimate.known && !confirm.loading && <div className="st-notice" style={{ margin: '10px 0 0' }}><Icon name="alert" size={14} /> <span>Le fournisseur n’a pas communiqué de tarif pour ces paramètres : la génération sera facturée sur vos crédits Higgsfield.</span></div>}
      <div className="st-modal-a"><Btn kind="ghost" onClick={() => setConfirm(null)}>Annuler</Btn><Btn onClick={doGenerate} disabled={confirm.loading} icon="bolt">Confirmer et lancer</Btn></div>
    </div></div>}
  </div>;
}
Object.assign(window, { CreativeStudio, StudioConnection });
