/* ===========================================================================
   iRaven App Store Studio — application shell: routing, project store,
   photo editor, brand editor, export.
   =========================================================================== */
const LS_KEY = 'socc360_studio_v4';
const IDX_KEY = 'iraven_projects_v1';
const PROJ_PREFIX = 'iraven_proj_';

/* ---------- project store ---------- */
function loadIndex() {
  try { const j = JSON.parse(localStorage.getItem(IDX_KEY) || '[]'); if (Array.isArray(j)) return j; } catch (e) {}
  return [];
}
function saveIndex(idx) { try { localStorage.setItem(IDX_KEY, JSON.stringify(idx)); } catch (e) {} }
function loadProjectState(id) {
  try { const j = JSON.parse(localStorage.getItem(PROJ_PREFIX + id) || 'null'); if (j && j.slides) return j; } catch (e) {}
  return null;
}
function saveProjectState(id, st) { try { localStorage.setItem(PROJ_PREFIX + id, JSON.stringify(st)); } catch (e) {} }

function newProjectId() { return 'p' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); }

function createProject(brand, slides, videos, assets) {
  const id = newProjectId();
  const idx = loadIndex();
  idx.unshift({ id, name: brand.name || 'App', brand,
    counts: { slides: slides.length, videos: videos.length } });
  saveIndex(idx);
  const st = { brand, slides, videos, assets: assets || [], current: 0, vcurrent: 0, tab: 'photos' };
  saveProjectState(id, st);
  if (window.__hasBackend) {
    fetch('/api/project/' + id, { method: 'PUT', headers: { 'content-type': 'application/json' },
      body: JSON.stringify(st) }).catch(() => {});
  }
  return id;
}

/* one-time migration of the original single-app Socc360 workspace */
function migrateLegacy() {
  if (loadIndex().length) return;
  const brand = { name: 'Socc360', tagline: 'Football from every angle.',
    accent1: '#4f86ff', accent2: '#f5a623' };
  let st = null;
  try { const p = JSON.parse(localStorage.getItem(LS_KEY) || 'null'); if (p && p.slides) st = p; } catch (e) {}
  const slides = (st && st.slides) || window.makeDefaultSlides();
  const videos = (st && st.videos) || window.makeDefaultVideos();
  const id = newProjectId();
  const idx = [{ id, name: 'Socc360', brand, counts: { slides: slides.length, videos: videos.length } }];
  saveIndex(idx);
  saveProjectState(id, { brand, slides, videos, assets: [],
    current: (st && st.current) || 0, vcurrent: (st && st.vcurrent) || 0, tab: (st && st.tab) || 'photos' });
}

/* concepts (AI wizard) → project */
function projectFromConcepts(c) {
  const tplCycle = ['headline-top', 'angled', 'callouts', 'poster', 'headline-bottom', 'big-stat', 'full-bleed', 'duo'];
  const bgCycle = ['stadium', 'ember', 'aurora', 'royal', 'floodlight', 'steel', 'dawn', 'minimal'];
  const brand = { name: c.name || 'App', tagline: c.tagline || '', logo: c.logo || null,
    accent1: /^#/.test(c.accent1 || '') ? c.accent1 : '#4f86ff',
    accent2: /^#/.test(c.accent2 || '') ? c.accent2 : '#f5a623' };
  const slides = (c.screens || []).map((s, i) => ({
    device: 'iphone', template: tplCycle[i % tplCycle.length], bg: bgCycle[i % bgCycle.length],
    accent: i % 2 ? 'brand2' : 'brand1', titleSize: 'md',
    headline: (s.headline || '').replace(/\\n/g, '\n'), subtitle: s.subtitle || '',
    callouts: '', image: null, image2: null,
  }));
  const vidTpl = ['rise', 'showcase', 'orbit'];
  const bgCycle2 = ['stadium', 'ember', 'aurora', 'royal', 'floodlight', 'steel', 'dawn', 'minimal'];
  const videos = (c.videos && c.videos.length ? c.videos : [{ headline: brand.name, subtitle: brand.tagline }])
    .map((v, i) => ({
      device: 'iphone', bg: bgCycle2[i % bgCycle2.length], accent: i % 2 ? 'brand2' : 'brand1',
      outro: true, name: 'Preview ' + String(i + 1).padStart(2, '0'),
      scenes: [window.makeScene({
        headline: (v.headline || '').replace(/\\n/g, '\n'), subtitle: v.subtitle || '',
        captionPos: 'top', dur: 3,
        enter: { type: 'rise', dur: 0.8, ease: 'outBack' }, exit: { type: 'fade', dur: 0.5, ease: 'in' },
      })],
    }));
  return createProject(brand, slides, videos, []);
}

/* ---------- photo slide thumbnail ---------- */
function Thumb({ slide, active, index, onClick }) {
  const dims = (window.DEVICES[slide.device] || window.DEVICES.iphone).canvas;
  const W = 116;
  const scale = W / dims.w;
  return (
    <button onClick={onClick} style={{
      position: 'relative', flex: '0 0 auto', padding: 0, cursor: 'pointer', borderRadius: 14, overflow: 'hidden',
      width: W, height: dims.h * scale, background: '#05070f',
      border: active ? '2px solid #2f6df6' : '2px solid rgba(255,255,255,0.08)',
      boxShadow: active ? '0 0 0 3px rgba(47,109,246,0.25)' : 'none', transition: 'all .15s' }}>
      <div style={{ width: dims.w, height: dims.h, transform: `scale(${scale})`, transformOrigin: 'top left' }}>
        <Panel slide={slide} />
      </div>
      <span style={{ position: 'absolute', top: 6, left: 6, fontFamily: "'JetBrains Mono',monospace", fontSize: 11,
        fontWeight: 700, color: '#fff', background: 'rgba(0,0,0,0.55)', borderRadius: 6, padding: '1px 6px' }}>
        {String(index + 1).padStart(2, '0')}
      </span>
    </button>
  );
}

/* ---------- photo editor (rail + stage + controls) ---------- */
function PhotoEditor({ slides, setSlides, current, setCurrent, exportHostRef, onStatus,
  assets, setAssets, pickMedia, assist }) {
  const slide = slides[Math.min(current, slides.length - 1)];
  const device = window.DEVICES[slide.device] || window.DEVICES.iphone;
  const dims = device.canvas;
  const stageRef = useRef(null);
  const [scale, setScale] = useState(0.3);

  useEffect(() => {
    const fit = () => {
      const el = stageRef.current;
      if (!el) return;
      const pad = 56;
      const s = Math.min((el.clientWidth - pad) / dims.w, (el.clientHeight - pad) / dims.h);
      setScale(Math.max(0.05, s));
    };
    fit();
    const ro = new ResizeObserver(fit);
    if (stageRef.current) ro.observe(stageRef.current);
    return () => ro.disconnect();
  }, [dims.w, dims.h]);

  const patch = (changes) => setSlides(arr => arr.map((s, i) => i === current ? { ...s, ...changes } : s));

  const addSlide = () => {
    if (slides.length >= 10) return;
    const ns = { device: 'iphone', template: 'headline-top', bg: 'stadium', accent: 'brand1',
      titleSize: 'md', headline: 'New screen', subtitle: '', callouts: '', image: null, image2: null };
    setSlides(arr => [...arr.slice(0, current + 1), ns, ...arr.slice(current + 1)]);
    setCurrent(current + 1);
  };
  const delSlide = () => {
    if (slides.length <= 1) return;
    setSlides(arr => arr.filter((_, i) => i !== current));
    setCurrent(c => Math.max(0, Math.min(c, slides.length - 2)));
  };
  const move = (dir) => {
    const j = current + dir;
    if (j < 0 || j >= slides.length) return;
    setSlides(arr => { const a = [...arr]; const t = a[current]; a[current] = a[j]; a[j] = t; return a; });
    setCurrent(j);
  };

  const onUpload = (field, file) => {
    if (!file) return;
    window.storeAsset(setAssets, { blob: file, kind: 'image', source: 'upload', name: file.name })
      .then(a => patch({ [field]: window.assetToRef(a) }))
      .catch(() => onStatus && onStatus('Could not store image'));
  };
  const chooseFromStorage = (field) => pickMedia({ accept: 'image', title: 'Screenshot', aiKind: 'image',
    onPick: (a) => patch({ [field]: window.assetToRef(a) }) });

  return (
    <div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
      <aside style={{ width: 156, flex: '0 0 auto', borderRight: '1px solid rgba(255,255,255,0.08)',
        display: 'flex', flexDirection: 'column', minHeight: 0 }}>
        <div style={{ flex: 1, overflowY: 'auto', padding: 16, display: 'flex', flexDirection: 'column',
          gap: 12, alignItems: 'center' }}>
          {slides.map((s, i) => (
            <Thumb key={i} slide={s} index={i} active={i === current} onClick={() => setCurrent(i)} />
          ))}
          <button onClick={addSlide} disabled={slides.length >= 10} style={{
            width: 116, padding: '14px 0', borderRadius: 14, cursor: slides.length >= 10 ? 'not-allowed' : 'pointer',
            border: '1px dashed rgba(255,255,255,0.18)', background: 'transparent',
            color: 'rgba(255,255,255,0.5)', fontFamily: BODY, fontSize: 13, opacity: slides.length >= 10 ? 0.4 : 1 }}>
            + Add screen
          </button>
        </div>
        <div style={{ padding: 12, borderTop: '1px solid rgba(255,255,255,0.08)', textAlign: 'center',
          fontFamily: "'JetBrains Mono',monospace", fontSize: 11, color: 'rgba(255,255,255,0.4)' }}>
          {slides.length} / 10 screens
        </div>
      </aside>

      <main ref={stageRef} style={{ flex: 1, position: 'relative', minWidth: 0,
        display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden',
        background: 'radial-gradient(120% 120% at 50% 0%, #0d1426, #05070f)' }}>
        <div style={{ width: dims.w * scale, height: dims.h * scale, position: 'relative',
          boxShadow: '0 40px 120px rgba(0,0,0,0.6)' }}>
          <div style={{ width: dims.w, height: dims.h, transform: `scale(${scale})`, transformOrigin: 'top left',
            overflow: 'hidden' }}>
            <Panel slide={slide} />
          </div>
        </div>
        <div style={{ position: 'absolute', bottom: 14, left: 0, right: 0, textAlign: 'center',
          fontFamily: "'JetBrains Mono',monospace", fontSize: 11, color: 'rgba(255,255,255,0.35)' }}>
          {device.label} · {dims.w}×{dims.h}px
        </div>
      </main>

      <aside style={{ width: 340, flex: '0 0 auto', borderLeft: '1px solid rgba(255,255,255,0.08)',
        overflowY: 'auto', padding: 22 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 18 }}>
          <span style={{ fontFamily: DISPLAY, fontWeight: 600, fontSize: 17 }}>Screen {String(current + 1).padStart(2, '0')}</span>
          <div style={{ display: 'flex', gap: 6 }}>
            <button title="AI for this screen" style={btnStyle({ padding: '6px 10px', fontSize: 12,
              background: 'rgba(47,109,246,0.18)', border: '1px solid rgba(47,109,246,0.4)', color: '#bcd2ff' })}
              onClick={() => assist({ kind: 'screenshot', current, slides,
                apply: (ch) => setSlides(arr => arr.map((s, i) => i === current ? { ...s, ...ch } : s)) })}>✦ AI</button>
            <button title="Move up" style={btnStyle({ padding: '6px 10px', fontSize: 13 })} onClick={() => move(-1)}>↑</button>
            <button title="Move down" style={btnStyle({ padding: '6px 10px', fontSize: 13 })} onClick={() => move(1)}>↓</button>
            <button title="Delete" style={btnStyle({ padding: '6px 10px', fontSize: 13, color: '#ff8089' })} onClick={delSlide}>✕</button>
          </div>
        </div>

        <Section title="Device">
          <Seg options={Object.values(window.DEVICES).map(d => ({ id: d.id, label: d.label }))}
            value={slide.device} onChange={v => patch({ device: v })} />
        </Section>

        <Section title="Template">
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
            {window.TEMPLATES.map(t => {
              const active = t.id === slide.template;
              return (
                <button key={t.id} onClick={() => patch({ template: t.id })} style={{
                  padding: '10px 8px', borderRadius: 10, cursor: 'pointer', fontFamily: BODY, fontSize: 12.5, fontWeight: 600,
                  border: active ? '1px solid #2f6df6' : '1px solid rgba(255,255,255,0.10)',
                  background: active ? 'rgba(47,109,246,0.18)' : 'rgba(255,255,255,0.03)',
                  color: active ? '#9dbcff' : 'rgba(255,255,255,0.72)' }}>{t.label}</button>
              );
            })}
          </div>
        </Section>

        <Section title="Background">
          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
            {window.BACKGROUNDS.map(b => {
              const active = b.id === slide.bg;
              return (
                <button key={b.id} title={b.label} onClick={() => patch({ bg: b.id })} style={{
                  width: 52, height: 52, borderRadius: 12, cursor: 'pointer', background: b.swatch,
                  border: active ? '2px solid #2f6df6' : '2px solid rgba(255,255,255,0.12)',
                  boxShadow: active ? '0 0 0 3px rgba(47,109,246,0.22)' : 'none' }} />
              );
            })}
          </div>
        </Section>

        <Section title="Accent">
          <Seg options={window.getAccentOptions()} value={slide.accent} onChange={v => patch({ accent: v })} cols={2} />
        </Section>

        <Section title="Headline size">
          <Seg options={window.TITLE_SIZES} value={slide.titleSize} onChange={v => patch({ titleSize: v })} />
        </Section>

        <Section title="Finishing" hint="device · tilt · glow">
          <div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: 6 }}>
            <Seg options={window.DEV_SIZES.map(d => ({ id: d.id, label: 'Device ' + d.label }))}
              value={slide.devSize || 'md'} onChange={v => patch({ devSize: v })} />
            <Seg options={window.TILTS.map(t => ({ id: t.id, label: 'Tilt ' + t.label }))}
              value={slide.tilt || 0} onChange={v => patch({ tilt: v })} />
            <Seg options={window.GLOWS.map(g => ({ id: g.id, label: 'Glow ' + g.label }))}
              value={slide.glow === undefined ? 0.5 : slide.glow} onChange={v => patch({ glow: v })} />
          </div>
        </Section>

        <Section title="Copy" hint="↵ = line break">
          <Field label="Headline" rows={2} value={slide.headline} placeholder="Every match. One place."
            onChange={v => patch({ headline: v })} />
          <Field label="Subtitle" rows={2} value={slide.subtitle} placeholder="One short supporting line."
            onChange={v => patch({ subtitle: v })} />
          {slide.template === 'callouts' ? (
            <Field label="Callouts (one per line, max 4)" rows={4} value={slide.callouts}
              placeholder={'Live timeline\nKey moments\nMatch stats'} onChange={v => patch({ callouts: v })} />
          ) : null}
        </Section>

        <Section title="Screenshot">
          <DropZone onFile={f => onUpload('image', f)} hasImage={!!slide.image} />
          <button style={btnStyle({ marginTop: 8, width: '100%', fontSize: 12.5 })}
            onClick={() => chooseFromStorage('image')}>Choose from storage</button>
          {slide.image ? (
            <button style={btnStyle({ marginTop: 6, width: '100%', color: '#ff8089' })}
              onClick={() => patch({ image: null })}>Remove screenshot</button>
          ) : null}
          {slide.template === 'duo' || slide.template === 'companion' || slide.template === 'fan' ? (
            <div style={{ marginTop: 10 }}>
              <DropZone compact onFile={f => onUpload('image2', f)} hasImage={!!slide.image2}
                title={slide.image2
                  ? (slide.template === 'companion' ? 'Watch screen · replace' : 'Second screen · replace')
                  : (slide.template === 'companion' ? 'Watch screen · drop screenshot' : 'Second screen · drop screenshot')} />
              <button style={btnStyle({ marginTop: 6, width: '100%', padding: '7px 10px', fontSize: 12 })}
                onClick={() => chooseFromStorage('image2')}>Choose from storage</button>
              {slide.image2 ? (
                <button style={btnStyle({ marginTop: 6, width: '100%', padding: '7px 10px', fontSize: 12, color: '#ff8089' })}
                  onClick={() => patch({ image2: null })}>Remove second screenshot</button>
              ) : null}
            </div>
          ) : null}
        </Section>
      </aside>
    </div>
  );
}

/* ---------- studio shell (one project) ---------- */
function StudioShell({ projId, onHome }) {
  const initRef = useRef(null);
  if (!initRef.current) {
    const st = loadProjectState(projId);
    initRef.current = st || { brand: window.CURRENT_BRAND, slides: window.makeBlankSlides(),
      videos: window.makeDefaultVideos(), assets: [], current: 0, vcurrent: 0, tab: 'photos' };
  }
  const init = initRef.current;
  const [brand, setBrand] = useState(init.brand || { name: 'App', tagline: '', accent1: '#4f86ff', accent2: '#f5a623' });
  const [showBrand, setShowBrand] = useState(false);
  window.CURRENT_BRAND = brand;   // accents + outro read from here

  const [slides, setSlides] = useState(init.slides);
  const [current, setCurrent] = useState(Math.min(init.current, init.slides.length - 1));
  /* migrate any legacy template-based previews into the scene model */
  const [videos, setVideos] = useState(() => (init.videos || []).map(window.videoToScenes));
  const [vcurrent, setVcurrent] = useState(Math.min(init.vcurrent, Math.max(0, (init.videos || []).length - 1)));
  const [assets, setAssets] = useState(() => {
    let a = init.assets || [];
    /* seed Socc360's shipped stock library into Storage the first time */
    const isSocc = (init.brand && init.brand.name) === 'Socc360';
    if (isSocc && !a.some(x => x.sub === 'bundled')) {
      a = [...window.bundledAssetRecords().filter(b => !a.some(x => x.path === b.path)), ...a];
    }
    return a;
  });
  const [tab, setTab] = useState(init.tab);
  const [exportMsg, setExportMsg] = useState(null);
  const [showCfg, setShowCfg] = useState(false);
  const [showStorage, setShowStorage] = useState(false);
  const [combineOpen, setCombineOpen] = useState(false);
  const [mediaPick, setMediaPick] = useState(null);   // {accept,title,aiKind,onPick}
  const [genState, setGenState] = useState(null);      // {kind,prompt,onAsset}
  const [assistState, setAssistState] = useState(null);// scope for AIAssist
  const exportHostRef = useRef(null);
  const exportRootRef = useRef(null);
  const videoStageRef = useRef(null);

  useEffect(() => {
    saveProjectState(projId, { brand, slides, videos, assets, current, vcurrent, tab });
    const idx = loadIndex();
    const it = idx.find(p => p.id === projId);
    if (it) { it.counts = { slides: slides.length, videos: videos.length }; it.brand = brand; it.name = brand.name; saveIndex(idx); }
    if (window.__hasBackend) {
      clearTimeout(window.__stateSyncT);
      window.__stateSyncT = setTimeout(() => {
        fetch('/api/project/' + projId, { method: 'PUT', headers: { 'content-type': 'application/json' },
          body: JSON.stringify({ brand, slides, videos, assets, current, vcurrent, tab }) }).catch(() => {});
      }, 1200);
    }
  }, [slides, current, videos, vcurrent, tab, brand, assets]);

  /* shared media helpers passed to both editors */
  const pickMedia = (opts) => setMediaPick(opts);
  const genMedia = (opts) => setGenState(opts || {});
  const assist = (scope) => setAssistState(scope);
  const storeUpload = async (file) => {
    if (!file) return null;
    if (file.type && file.type.startsWith('video')) {
      setExportMsg('Checking video…');
      const p = await window.vidProbeVideo(file);
      if (!p.ok) { setExportMsg(window.VID_CODEC_MSG); setTimeout(() => setExportMsg(null), 9000); return null; }
      const a = await window.storeAsset(setAssets, { blob: file, kind: 'video', source: 'upload', name: file.name, dur: p.dur });
      setExportMsg(null); return a;
    }
    return await window.storeAsset(setAssets, { blob: file,
      kind: file.type && file.type.startsWith('audio') ? 'audio' : 'image', source: 'upload', name: file.name });
  };

  useEffect(() => {
    if (exportHostRef.current && !exportRootRef.current) {
      exportRootRef.current = ReactDOM.createRoot(exportHostRef.current);
    }
  }, []);

  /* re-index any media used by this project that lost its Storage record
     (e.g. soundtracks/clips added through older flows) so it shows in Storage */
  useEffect(() => {
    let live = true;
    window.reconcileAssets({ slides, videos, assets }, (fn) => { if (live) setAssets(fn); });
    return () => { live = false; };
    // eslint-disable-next-line
  }, []);

  /* photo export — selected screen only, exact App Store pixels */
  const renderToHost = (sl) => new Promise(async (resolve) => {
    const host = exportHostRef.current;
    exportRootRef.current.render(null);
    for (let i = 0; i < 400 && host.firstChild; i++) await vidYield();
    exportRootRef.current.render(<Panel slide={sl} />);
    for (let i = 0; i < 800 && !host.firstChild; i++) await vidYield();
    const imgs = host.querySelectorAll('img');
    await Promise.all([...imgs].map(im => im.complete && im.naturalWidth
      ? Promise.resolve() : new Promise(r => { im.onload = im.onerror = r; })));
    try { await document.fonts.ready; } catch (e) {}
    resolve();
  });

  const exportPhoto = async () => {
    const sl = slides[current];
    const d = (window.DEVICES[sl.device] || window.DEVICES.iphone).canvas;
    setExportMsg('Rendering…');
    try {
      await window.mediaPrewarm([sl.image, sl.image2]);
      await renderToHost(sl);
      const fontCss = await getFontEmbedCss();
      const url = await window.htmlToImage.toPng(exportHostRef.current.firstChild, {
        width: d.w, height: d.h, pixelRatio: 1, backgroundColor: '#03050e',
        fontEmbedCSS: fontCss, style: { position: 'static', left: '0px', top: '0px', margin: '0' },
      });
      const a = document.createElement('a');
      a.href = url;
      a.download = `${(brand.name || 'app').toLowerCase().replace(/[^a-z0-9]+/g, '')}-${sl.device}-${String(current + 1).padStart(2, '0')}.png`;
      a.click();
      try { await window.storeAsset(setAssets, { dataUrl: url, kind: 'image', source: 'export', sub: 'screenshot',
        name: `${(brand.name || 'app').toLowerCase().replace(/[^a-z0-9]+/g, '')}-${String(current + 1).padStart(2, '0')}.png` }); } catch (e) {}
      setExportMsg('Saved ✓');
    } catch (e) { setExportMsg('Export failed'); console.error(e); }
    setTimeout(() => setExportMsg(null), 1600);
  };

  const exportVideo = () => { videoStageRef.current && videoStageRef.current.record(); };
  const savePreview = () => { videoStageRef.current && videoStageRef.current.save && videoStageRef.current.save(); };
  // App Store-ready export: runs the rendered mp4 through the server-side
  // ffmpeg pipeline (`/api/export`) so the output is guaranteed constant
  // 30 fps, H.264 high@4.0, AAC stereo, capped to 30s. App Store Connect
  // rejects the raw `Export video` output because the realtime encoder
  // can drift to <30 fps avg on slow CPUs.
  const [appStorePreset, setAppStorePreset] = useState(
    typeof window !== 'undefined' && window.APP_STORE_DEFAULT_PRESET ? window.APP_STORE_DEFAULT_PRESET : 'iphone-6-5-portrait');
  const exportAppStore = () => {
    videoStageRef.current && videoStageRef.current.recordAppStore && videoStageRef.current.recordAppStore(appStorePreset);
  };
  // Record on server — the deterministic offline path. Walks the timeline
  // frame-by-frame, captures each frame as JPEG, zips, ships to /api/render
  // where ffmpeg image2 assembles a CFR 30 mp4. Use this whenever the
  // browser-side WebCodecs export shows repeated/missing frames (live
  // <video> seek drift, slow GPU, etc.).
  const recordOnServer = () => {
    videoStageRef.current && videoStageRef.current.recordOnServer && videoStageRef.current.recordOnServer(appStorePreset);
  };

  const tabBtn = (id, label) => {
    const active = tab === id;
    return (
      <button onClick={() => setTab(id)} style={{
        padding: '8px 18px', borderRadius: 10, cursor: 'pointer', fontFamily: BODY, fontSize: 13.5, fontWeight: 600,
        border: 'none', background: active ? 'rgba(47,109,246,0.2)' : 'transparent',
        color: active ? '#9dbcff' : 'rgba(255,255,255,0.55)', transition: 'all .15s' }}>
        {label}
      </button>
    );
  };

  return (
    <div style={{ position: 'fixed', inset: 0, display: 'flex', flexDirection: 'column',
      background: '#070a14', color: '#fff', fontFamily: BODY }}>
      <header style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 22px',
        height: 60, borderBottom: '1px solid rgba(255,255,255,0.08)', flex: '0 0 auto' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
          <button title="All apps" style={btnStyle({ padding: '6px 12px' })} onClick={onHome}>←</button>
          <button title="Edit brand" onClick={() => setShowBrand(true)} style={{ display: 'flex', alignItems: 'center',
            gap: 10, border: 'none', background: 'transparent', cursor: 'pointer', padding: 0 }}>
            {brand.logo ? (
              <img src={brand.logo} style={{ height: 28, maxWidth: 90, objectFit: 'contain' }} />
            ) : null}
            <span style={{ fontFamily: DISPLAY, fontWeight: 700, fontSize: 22, letterSpacing: '-0.02em' }}>
              {(() => { const [a, b] = window.splitBrandName(brand.name);
                return (<>
                  <span style={{ color: brand.accent1 || '#4f86ff' }}>{a}</span>
                  <span style={{ color: brand.accent2 || '#f5a623' }}>{b}</span>
                </>); })()}
            </span>
          </button>
          <span style={{ width: 1, height: 22, background: 'rgba(255,255,255,0.15)' }} />
          <span style={{ fontSize: 13.5, color: 'rgba(255,255,255,0.55)' }}>iRaven Studio</span>
          <div style={{ display: 'flex', gap: 4, marginLeft: 14, padding: 3, borderRadius: 12,
            background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.08)' }}>
            {tabBtn('photos', 'Screenshots')}
            {tabBtn('videos', 'App Previews')}
          </div>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          {exportMsg ? <span style={{ fontSize: 12.5, color: '#9dbcff', fontFamily: "'JetBrains Mono',monospace", maxWidth: 340, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{exportMsg}</span> : null}
          <button title="AI settings" style={btnStyle({ padding: '8px 12px' })} onClick={() => setShowCfg(true)}>⚙</button>
          <button style={btnStyle()} onClick={() => setShowStorage(true)}>🗂 Storage <span style={{ opacity: 0.55, fontFamily: "'JetBrains Mono',monospace", fontSize: 11 }}>{assets.length}</span></button>
          <button style={btnStyle()} onClick={() => setGenState({})}>✦ AI media</button>
          <button title="Save the whole project to a file you can reopen later"
            style={btnStyle()} onClick={() => window.exportProjectFile(
              { brand, slides, videos, assets, current, vcurrent, tab }, setExportMsg)}>💾 Save</button>
          {tab === 'photos' ? (
            <button style={btnStyle({ background: 'linear-gradient(180deg,#3b78ff,#1f54d6)', border: '1px solid #3b78ff' })}
              onClick={exportPhoto}>Export PNG · selected</button>
          ) : (
            <>
              <button style={btnStyle()} onClick={() => setCombineOpen(true)} disabled={videos.length < 2}>⧉ Combine</button>
              <button title="Render this preview and save it to the server — appears in Saved Previews"
                style={btnStyle({ background: 'rgba(47,109,246,0.18)', border: '1px solid rgba(47,109,246,0.4)', color: '#bcd2ff' })}
                onClick={savePreview}>💾 Save preview</button>
              <button title="Raw render — useful for inspection, NOT App Store-compatible. Use the App Store button for uploads."
                style={btnStyle()} onClick={exportVideo}>Export raw</button>
              <select value={appStorePreset} onChange={e => setAppStorePreset(e.target.value)}
                title="App Store Connect target. Server normalises to constant 30 fps, H.264, AAC, max 30s."
                style={{ ...btnStyle({ padding: '8px 12px', cursor: 'pointer' }), appearance: 'none', minWidth: 220 }}>
                {(window.APP_STORE_PRESETS || []).map(p => (
                  <option key={p.key} value={p.key} style={{ background: '#0c1222', color: '#fff' }}>{p.label}</option>
                ))}
              </select>
              <button title="Browser-recorded mp4 then normalised on the server. Fast, but live videos can show stuck/repeated frames on slow GPUs. Switch to Record on server if that happens."
                style={btnStyle({ background: 'linear-gradient(180deg,#3b78ff,#1f54d6)', border: '1px solid #3b78ff' })}
                onClick={exportAppStore}>🍎 Export App Store</button>
              <button title="Render every frame deterministically in the browser, ship to /api/render where ffmpeg image2 assembles a guaranteed CFR 30 mp4. Slower upload (~30 MB) but no dropped frames."
                style={btnStyle({ background: 'linear-gradient(180deg,#2dbd6b,#1d8f50)', border: '1px solid #2dbd6b' })}
                onClick={recordOnServer}>🎬 Record on server</button>
            </>
          )}
        </div>
      </header>

      {tab === 'photos' ? (
        <PhotoEditor slides={slides} setSlides={setSlides} current={current} setCurrent={setCurrent}
          exportHostRef={exportHostRef} onStatus={setExportMsg}
          assets={assets} setAssets={setAssets} pickMedia={pickMedia} assist={assist} />
      ) : (
        <VideoEditor videos={videos} setVideos={setVideos} current={vcurrent} setCurrent={setVcurrent}
          stageRef={videoStageRef} onStatus={setExportMsg}
          assets={assets} setAssets={setAssets} pickMedia={pickMedia} genMedia={genMedia}
          assist={(scope) => assist({ ...scope, applyAudio: (ref) => setVideos(arr => arr.map((v, i) => i === vcurrent ? { ...v, audio: ref } : v)) })} />
      )}

      <div ref={exportHostRef} style={{ position: 'fixed', left: -20000, top: 0, pointerEvents: 'none' }} />
      {showBrand ? (
        <BrandEditor brand={brand} onClose={() => setShowBrand(false)}
          onSave={(b) => { setBrand(b); setShowBrand(false); }} />
      ) : null}
      {showCfg ? <AISettings onClose={() => setShowCfg(false)} /> : null}

      {showStorage ? (
        <StoragePanel assets={assets} setAssets={setAssets} onClose={() => setShowStorage(false)}
          onUpload={storeUpload} onGenerate={() => { setShowStorage(false); setGenState({}); }} />
      ) : null}

      {combineOpen ? (
        <CombineModal videos={videos} assets={assets} setAssets={setAssets} onStatus={setExportMsg}
          onClose={() => setCombineOpen(false)} />
      ) : null}

      {mediaPick ? (
        <StoragePicker assets={assets} accept={mediaPick.accept} title={mediaPick.title}
          onPick={(a) => { mediaPick.onPick(a); setMediaPick(null); }}
          onClose={() => setMediaPick(null)}
          onUpload={async (f) => { const a = await storeUpload(f); if (a) { mediaPick.onPick(a); setMediaPick(null); } }}
          onGenerate={mediaPick.aiKind ? () => setGenState({ kind: mediaPick.aiKind,
            onAsset: (a) => { mediaPick.onPick(a); setMediaPick(null); } }) : null} />
      ) : null}

      {genState ? (
        <AIMediaModal context={{ tab }} initial={{ kind: genState.kind, prompt: genState.prompt }}
          assets={assets} setAssets={setAssets}
          onUse={genState.onAsset ? async (a) => { await genState.onAsset(a); } : null}
          onClose={() => setGenState(null)} onStatus={setExportMsg} />
      ) : null}

      {assistState ? (
        <AIAssist scope={assistState} brand={brand} assets={assets} setAssets={setAssets}
          genMedia={genMedia} pickMedia={pickMedia}
          onClose={() => setAssistState(null)} onStatus={setExportMsg} />
      ) : null}
    </div>
  );
}

/* ---------- brand editor (name / tagline / colors / logo) ---------- */
function BrandEditor({ brand, onSave, onClose }) {
  const [b, setB] = useState({ ...brand });
  const colorField = (key, label) => (
    <label style={{ display: 'flex', alignItems: 'center', gap: 8, flex: 1 }}>
      <span style={{ width: 26, height: 26, borderRadius: 8, background: b[key] || '#888',
        border: '1px solid rgba(255,255,255,0.25)', flex: '0 0 auto' }} />
      <input value={b[key] || ''} placeholder={label} onChange={e => setB({ ...b, [key]: e.target.value })}
        style={{ width: '100%', padding: '8px 10px', borderRadius: 9, background: 'rgba(255,255,255,0.04)',
          border: '1px solid rgba(255,255,255,0.10)', color: '#fff', fontFamily: "'JetBrains Mono',monospace",
          fontSize: 12.5, outline: 'none' }} />
    </label>
  );
  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 1200, background: 'rgba(3,5,14,0.8)',
      display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <div onClick={e => e.stopPropagation()} style={{ width: 'min(460px, 92vw)', borderRadius: 16,
        background: '#0c1222', border: '1px solid rgba(255,255,255,0.12)', padding: 22 }}>
        <div style={{ fontFamily: DISPLAY, fontWeight: 600, fontSize: 17, color: '#fff', marginBottom: 14 }}>Brand</div>
        <Field label="App name" value={b.name || ''} onChange={v => setB({ ...b, name: v })} />
        <Field label="Tagline" value={b.tagline || ''} onChange={v => setB({ ...b, tagline: v })} />
        <div style={{ display: 'flex', gap: 12, marginBottom: 12 }}>
          {colorField('accent1', '#accent1')}
          {colorField('accent2', '#accent2')}
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
          {b.logo ? (
            <img src={b.logo} style={{ height: 44, maxWidth: 130, objectFit: 'contain', borderRadius: 8,
              background: 'rgba(255,255,255,0.06)', padding: 5, flex: '0 0 auto' }} />
          ) : null}
          <div style={{ flex: 1 }}>
            <DropZone compact accept="image/*" sub="PNG / SVG · used in the video outro"
              title={b.logo ? 'Logo · replace' : 'Logo · drop image (optional)'} hasImage={!!b.logo}
              onFile={f => { if (!f) return; const r = new FileReader();
                r.onload = () => setB(prev => ({ ...prev, logo: r.result })); r.readAsDataURL(f); }} />
          </div>
          {b.logo ? (
            <button style={btnStyle({ padding: '7px 10px', fontSize: 12, color: '#ff8089', flex: '0 0 auto' })}
              onClick={() => setB({ ...b, logo: null })}>Remove</button>
          ) : null}
        </div>
        <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
          <button style={btnStyle()} onClick={onClose}>Cancel</button>
          <button style={btnStyle({ background: 'linear-gradient(180deg,#3b78ff,#1f54d6)', border: '1px solid #3b78ff' })}
            onClick={() => onSave(b)}>Save</button>
        </div>
      </div>
    </div>
  );
}

/* ---------- root app: home ⇄ wizard ⇄ studio ---------- */
function App() {
  const [route, setRoute] = useState(() => {
    const last = localStorage.getItem('iraven_last_proj');
    if (last && loadProjectState(last)) return { view: 'studio', id: last };
    return { view: 'home' };
  });
  const [, force] = useState(0);

  const open = (id) => { localStorage.setItem('iraven_last_proj', id); setRoute({ view: 'studio', id }); };
  const goHome = () => { localStorage.removeItem('iraven_last_proj'); setRoute({ view: 'home' }); };

  if (route.view === 'studio') {
    return <StudioShell key={route.id} projId={route.id} onHome={goHome} />;
  }
  if (route.view === 'wizard') {
    return <AIWizard
      onCancel={() => setRoute({ view: 'home' })}
      onCreate={(concepts) => { const id = projectFromConcepts(concepts); open(id); }} />;
  }
  return <HomePage projects={loadIndex()}
    onOpen={open}
    onNewAI={() => setRoute({ view: 'wizard' })}
    onNewBlank={() => {
      const brand = { name: 'My App', tagline: '', accent1: '#4f86ff', accent2: '#f5a623' };
      const id = createProject(brand, window.makeBlankSlides(), window.makeBlankVideos());
      open(id);
    }}
    onOpenFile={async (file) => {
      try {
        const st = await window.importProjectFile(file);
        const id = createProject(st.brand || { name: st.brand && st.brand.name || 'App' },
          st.slides || window.makeBlankSlides(), st.videos || window.makeBlankVideos(), st.assets || []);
        /* persist the full restored state (createProject seeds defaults; overwrite with the real thing) */
        saveProjectState(id, { brand: st.brand, slides: st.slides, videos: st.videos, assets: st.assets,
          current: st.current || 0, vcurrent: st.vcurrent || 0, tab: st.tab || 'photos' });
        const idx = loadIndex(); const it = idx.find(p => p.id === id);
        if (it) { it.brand = st.brand; it.name = (st.brand && st.brand.name) || 'App';
          it.counts = { slides: (st.slides || []).length, videos: (st.videos || []).length }; saveIndex(idx); }
        if (window.__hasBackend) fetch('/api/project/' + id, { method: 'PUT', headers: { 'content-type': 'application/json' },
          body: JSON.stringify({ brand: st.brand, slides: st.slides, videos: st.videos, assets: st.assets,
            current: st.current || 0, vcurrent: st.vcurrent || 0, tab: st.tab || 'photos' }) }).catch(() => {});
        open(id);
      } catch (e) { alert('Could not open project file:\n' + (e.message || e)); }
    }}
    onDelete={(id) => {
      if (!confirm('Delete this app and all its screens?')) return;
      /* release every blob this project holds (uploads, AI, exports, scene media, audio) */
      const st = loadProjectState(id);
      const keys = new Set();
      for (const a of (st && st.assets) || []) if (a.key) keys.add(a.key);
      for (const v of (st && st.videos) || []) {
        for (const sc of v.scenes || []) { const m = sc.media; if (m && typeof m === 'object' && m.key) keys.add(m.key); }
        for (const m of v.images || []) { if (m && typeof m === 'object' && m.key) keys.add(m.key); }
        if (v.audio && v.audio.key) keys.add(v.audio.key);
      }
      for (const sl of (st && st.slides) || []) {
        for (const f of ['image', 'image2']) { const m = sl[f]; if (m && typeof m === 'object' && m.key) keys.add(m.key); }
      }
      keys.forEach(k => vidMediaDel(k));
      saveIndex(loadIndex().filter(p => p.id !== id));
      try { localStorage.removeItem(PROJ_PREFIX + id); } catch (e) {}
      if (window.__hasBackend) fetch('/api/project/' + id, { method: 'DELETE' }).catch(() => {});
      force(x => x + 1);
    }} />;
}

/* boot: detect the local backend (Docker server.js); if present and there is
   no local work yet, restore the last saved state from it. Falls back to
   pure-localStorage mode (preview / standalone file) when absent. */
async function bootStudio() {
  window.__hasBackend = false;
  try {
    const ctl = new AbortController();
    const t = setTimeout(() => ctl.abort(), 1500);
    const r = await fetch('/api/health', { signal: ctl.signal });
    clearTimeout(t);
    if (r.ok) {
      const j = await r.json();
      window.__hasBackend = !!(j && j.ok);
    }
  } catch (e) {}
  if (window.__hasBackend && !localStorage.getItem(IDX_KEY)) {
    try {
      const r = await fetch('/api/state');
      const j = await r.json();
      if (j && j.index && j.index.length) {
        saveIndex(j.index);
        for (const p of j.index) if (j.projects && j.projects[p.id]) saveProjectState(p.id, j.projects[p.id]);
      } else if (j && j.slides && j.slides.length) {
        localStorage.setItem(LS_KEY, JSON.stringify(j));   // legacy single-app backend state
      }
    } catch (e) {}
  }
  migrateLegacy();
  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
}
bootStudio();
