/* Webinaire — application principale : modes (Slides / Tableau blanc / Lecture),
   grille 0 à 3 caméras, stylet, enregistrement. Esthétique charte Yéhi-Or. */

const { useState, useEffect, useRef } = React;

const WB_KEY = 'yehi-or.webinaire.v1';
function wbLoad() { try { const s = localStorage.getItem(WB_KEY); return s ? JSON.parse(s) : {}; } catch { return {}; } }
function wbSave(d) { try { localStorage.setItem(WB_KEY, JSON.stringify(d)); } catch (e) {} }

const MODES = [
  { id: 'slides',  label: 'Slides' },
  { id: 'board',   label: 'Tableau blanc' },
  { id: 'bible',   label: 'Bible' },
];

// ===================== APPLICATION =====================
function WebinaireApp() {
  const saved = wbLoad();
  const [mode, setMode]         = useState(saved.mode || 'slides');
  const [camCount, setCamCount] = useState(saved.camCount != null ? saved.camCount : 1);
  const [cams, setCams]         = useState(saved.cams || [
    { source: 'local',       deviceId: null, bgPreset: 'none' },
    { source: 'placeholder', deviceId: null, bgPreset: 'none' },
    { source: 'placeholder', deviceId: null, bgPreset: 'none' },
  ]);
  const [camShape, setCamShape] = useState(saved.camShape || 'rounded');
  const [mirror, setMirror]     = useState(saved.mirror != null ? saved.mirror : true);
  const [title, setTitle]       = useState(saved.title != null ? saved.title : '');
  const [dragOver, setDragOver] = useState(false);

  const [slides, setSlides]     = useState([]);     // non persisté (volumineux)
  const [slideIdx, setSlideIdx] = useState(0);
  const [importing, setImporting] = useState(false);

  const [draw, setDraw]         = useState(DEFAULT_DRAW_STATE);
  const [strokesMap, setStrokesMap] = useState({});
  const [slidesRecv, setSlidesRecv] = useState(null);   // {have,total} : réception des slides d'un pair

  const [bibleBook, setBibleBook]       = useState(saved.bibleBook || 'Gen');
  const [bibleChapter, setBibleChapter] = useState(saved.bibleChapter || 1);
  const [activeVerse, setActiveVerse]   = useState(1);
  const [bibleStrong, setBibleStrong]   = useState(null);   // {code, word} : popup Strong's partagé
  const [bibleReady, setBibleReady]     = useState(false);

  const [panelOpen, setPanelOpen] = useState(true);
  const [scale, setScale]       = useState(1);
  const [idle, setIdle]         = useState(false);
  const [recording, setRecording] = useState(false);
  const [installPrompt, setInstallPrompt] = useState(null);

  const stageRef = useRef(null);
  const recRef   = useRef(null);
  const room     = useMesh();
  const myDrawId = useRef('u' + Math.random().toString(36).slice(2, 8));   // auteur de mes traits

  // Diffuse un changement d'état à tous les participants de la salle (no-op hors salle).
  const sync = (msg) => room.sendSync(msg);

  // Persistance (réglages légers seulement)
  useEffect(() => { wbSave({ mode, camCount, cams, camShape, mirror, title, bibleBook, bibleChapter }); },
    [mode, camCount, cams, camShape, mirror, title, bibleBook, bibleChapter]);

  // Charge les données Bible à la demande (au 1er passage en mode Bible)
  useEffect(() => {
    if (mode === 'bible' && !bibleReady) loadBibleData().then(() => setBibleReady(true)).catch(() => {});
  }, [mode, bibleReady]);

  // Mise à l'échelle du plateau 1920×1080
  useEffect(() => {
    const fit = () => {
      if (!stageRef.current) return;
      const r = stageRef.current.getBoundingClientRect();
      const s = Math.min(r.width / 1920, r.height / 1080);
      setScale(s > 0 ? s : 1);
    };
    fit();
    const ro = new ResizeObserver(fit);
    if (stageRef.current) ro.observe(stageRef.current);
    window.addEventListener('resize', fit);
    return () => { ro.disconnect(); window.removeEventListener('resize', fit); };
  }, [panelOpen]);

  // Surface de dessin courante
  const surfaceKey = mode === 'slides' ? ('slide:' + slideIdx) : mode;
  const strokes = strokesMap[surfaceKey] || [];
  const strokesRef = useRef(strokes); strokesRef.current = strokes;   // toujours à jour (clavier)

  // Applique une opération de dessin (sans rediffusion). Fusionne par identifiant de trait,
  // pour que plusieurs personnes puissent dessiner en même temps sans s'effacer mutuellement.
  const applyDrawOp = (key, op) => {
    setStrokesMap(m => {
      const cur = m[key] || [];
      let next;
      if (op.t === 'clear') next = [];
      else if (op.t === 'remove') { const ids = new Set(op.ids); next = cur.filter(s => !ids.has(s.id)); }
      else if (op.t === 'upsert') {
        const i = cur.findIndex(s => s.id === op.stroke.id);
        if (i === -1) {
          // Nouveau trait : ordre d'empilement déterministe (même chez tous) par (ts, id).
          next = [...cur, op.stroke].sort((a, b) => (a.ts || 0) - (b.ts || 0) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
        } else { next = cur.slice(); next[i] = op.stroke; }   // mise à jour en place (trait en cours)
      } else next = cur;
      return { ...m, [key]: next };
    });
  };

  // Opérations locales : on applique en local ET on diffuse.
  const drawLive   = (stroke) => sync({ kind: 'draw', key: surfaceKey, op: { t: 'upsert', stroke } });
  const drawCommit = (stroke) => { applyDrawOp(surfaceKey, { t: 'upsert', stroke }); sync({ kind: 'draw', key: surfaceKey, op: { t: 'upsert', stroke } }); };
  const drawRemove = (ids)    => { applyDrawOp(surfaceKey, { t: 'remove', ids }); sync({ kind: 'draw', key: surfaceKey, op: { t: 'remove', ids } }); };
  const drawClear  = ()       => { applyDrawOp(surfaceKey, { t: 'clear' }); sync({ kind: 'draw', key: surfaceKey, op: { t: 'clear' } }); };
  const drawUndo   = () => {
    const mine = strokesRef.current.filter(s => s.author === myDrawId.current);
    if (mine.length) drawRemove([mine[mine.length - 1].id]);
  };

  const goToSlide = (i) => {
    const n = Math.max(0, Math.min(i, Math.max(0, slides.length - 1)));
    setSlideIdx(n);
    sync({ kind: 'slide', idx: n });
  };
  const nextSlide = () => goToSlide(slideIdx + 1);
  const prevSlide = () => goToSlide(slideIdx - 1);

  // Changement de mode (Slides / Tableau / Bible) partagé
  const changeMode = (m) => { setMode(m); sync({ kind: 'mode', mode: m }); };
  // Titre partagé
  const changeTitle = (t) => { setTitle(t); sync({ kind: 'title', title: t }); };

  // Raccourcis clavier
  useEffect(() => {
    const onKey = (e) => {
      const t = e.target.tagName;
      if (t === 'INPUT' || t === 'TEXTAREA' || t === 'SELECT') return;
      if (e.key === 'd' || e.key === 'D') { setDraw(s => ({ ...s, active: !s.active })); }
      else if (e.key === 'f' || e.key === 'F') {
        document.fullscreenElement ? document.exitFullscreen() : document.documentElement.requestFullscreen();
      }
      else if (e.key === 'e' || e.key === 'E') { setPanelOpen(o => !o); }
      else if ((e.ctrlKey || e.metaKey) && (e.key === 'z' || e.key === 'Z')) { e.preventDefault(); drawUndo(); }
      else if (mode === 'slides') {
        if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ') { e.preventDefault(); nextSlide(); }
        else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); prevSlide(); }
      }
      else if (mode === 'bible') {
        const max = (window.BIBLE_INDEX && window.BIBLE_INDEX[bibleBook] && window.BIBLE_INDEX[bibleBook].verses[bibleChapter]) || 999;
        if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { selectVerse(Math.min(activeVerse + 1, max)); }
        else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { selectVerse(Math.max(activeVerse - 1, 1)); }
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [mode, slides.length, slideIdx, bibleBook, bibleChapter, activeVerse, surfaceKey]);

  // Curseur masqué en mode présentation (panneau fermé)
  useEffect(() => {
    if (panelOpen) { setIdle(false); return; }
    let timer;
    const reset = () => { setIdle(false); clearTimeout(timer); timer = setTimeout(() => setIdle(true), 2500); };
    reset();
    window.addEventListener('mousemove', reset);
    window.addEventListener('keydown', reset);
    return () => { clearTimeout(timer); window.removeEventListener('mousemove', reset); window.removeEventListener('keydown', reset); };
  }, [panelOpen]);

  // PWA install
  useEffect(() => {
    if ('serviceWorker' in navigator) navigator.serviceWorker.register('service-worker.js').catch(() => {});
    const onP = (e) => { e.preventDefault(); setInstallPrompt(e); };
    window.addEventListener('beforeinstallprompt', onP);
    return () => window.removeEventListener('beforeinstallprompt', onP);
  }, []);

  const importSlides = async (files) => {
    if (!files || !files.length) return;
    setImporting(true);
    try {
      const added = await importSlideFiles(files);
      const wasEmpty = slides.length === 0;
      const all = [...slides, ...added];
      setSlides(all);
      if (wasEmpty) setSlideIdx(0);
      // Diffusion robuste : un message par slide (évite un envoi unique trop volumineux).
      if (room.role) {
        added.forEach(slide => sync({ kind: 'slide-append', slide, total: all.length }));
        if (wasEmpty) sync({ kind: 'slide', idx: 0 });
      }
    } catch (e) { alert('Import impossible : ' + e.message); }
    setImporting(false);
  };
  const clearSlides = () => { setSlides([]); setSlideIdx(0); sync({ kind: 'slides-clear' }); };

  // ---- Enregistrement (capture de la fenêtre + audio) ----
  // Note audio : on ne mélange PAS l'audio système avec le micro.
  // Mélanger les deux crée un écho/phasing (le micro capte les haut-parleurs
  // qui diffusent les participants → bruit strident). L'audio système de
  // Enregistrement : vidéo écran + mix audio direct des flux WebRTC (sans loopback système).
  // AudioContext mixe : micro local + flux audio de chaque participant → propre, pas de bruit parasite.
  const startRec = async () => {
    try {
      // 1. Capture vidéo écran uniquement (pas d'audio système)
      const disp = await navigator.mediaDevices.getDisplayMedia({
        video: { frameRate: 30 }, audio: false,
      });

      // 2. AudioContext : mixe tous les flux audio directement
      const ctx = new (window.AudioContext || window.webkitAudioContext)();
      const dest = ctx.createMediaStreamDestination();
      let hasAudio = false;

      const connectStream = (s) => {
        if (!s) return;
        const tracks = s.getAudioTracks ? s.getAudioTracks() : [];
        if (!tracks.length) return;
        try {
          ctx.createMediaStreamSource(new MediaStream(tracks)).connect(dest);
          hasAudio = true;
        } catch (e) {}
      };

      // Micro local
      connectStream(room.localStream);
      // Flux audio de chaque participant connecté
      (room.participants || []).forEach(p => connectStream(p.stream));

      // Si aucun flux WebRTC (solo, test sans salle) → tenter micro système
      if (!hasAudio) {
        try {
          const mic = await navigator.mediaDevices.getUserMedia({
            audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
          });
          connectStream(mic);
        } catch (e) {}
      }

      const audioTracks = dest.stream.getAudioTracks();
      const stream = new MediaStream([...disp.getVideoTracks(), ...audioTracks]);
      const mime = MediaRecorder.isTypeSupported('video/webm;codecs=vp9,opus')
        ? 'video/webm;codecs=vp9,opus' : 'video/webm';
      const rec = new MediaRecorder(stream, { mimeType: mime });
      const chunks = [];
      rec.ondataavailable = (e) => { if (e.data && e.data.size) chunks.push(e.data); };
      rec.onstop = () => {
        const blob = new Blob(chunks, { type: 'video/webm' });
        const a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        a.download = 'webinaire-yehi-or-' + new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-') + '.webm';
        a.click();
        disp.getTracks().forEach(t => t.stop());
        ctx.close();
      };
      disp.getVideoTracks()[0].addEventListener('ended', () => {
        if (recRef.current && recRef.current.state !== 'inactive') { recRef.current.stop(); setRecording(false); }
      });
      rec.start(1000);
      recRef.current = rec;
      setRecording(true);
    } catch (e) { alert('Enregistrement annulé ou impossible : ' + e.message); }
  };
  const stopRec = () => { try { recRef.current && recRef.current.stop(); } catch (e) {} setRecording(false); };

  const triggerInstall = async () => { if (!installPrompt) return; installPrompt.prompt(); await installPrompt.userChoice; setInstallPrompt(null); };

  // Réunion : en salle, les emplacements caméra affichent tous les participants
  const roomTiles = room.role ? [
    { id: 'self', stream: room.localStream, name: 'Moi', muted: true, mirror: mirror },
    ...room.participants.map(p => ({ id: p.id, stream: p.stream, name: p.name, muted: false, mirror: false })),
  ] : null;
  const effectiveCamCount = roomTiles ? roomTiles.length : camCount;
  // Géométrie : la zone de présentation laisse la place à la colonne caméras (si > 0)
  const presRight = effectiveCamCount > 0 ? (48 + 360 + 24) : 48;

  // ---- Navigation Bible partagée ----
  const goToRef = (osis, ch, v) => {
    setBibleBook(osis); setBibleChapter(ch); setActiveVerse(v || 1); setBibleStrong(null);
    sync({ kind: 'bible', book: osis, chapter: ch, verse: v || 1 });
  };
  const selectVerse = (v) => { setActiveVerse(v); sync({ kind: 'verse', verse: v }); };
  const changeBook = (osis) => {
    setBibleBook(osis); setBibleChapter(1); setActiveVerse(1); setBibleStrong(null);
    sync({ kind: 'bible', book: osis, chapter: 1, verse: 1 });
  };
  const changeChapter = (ch) => {
    setBibleChapter(ch); setActiveVerse(1); setBibleStrong(null);
    sync({ kind: 'bible', book: bibleBook, chapter: ch, verse: 1 });
  };
  // Popup Strong's (clic sur un mot hébreu) partagé
  const changeStrong = (s) => { setBibleStrong(s); sync({ kind: 'strong', strong: s }); };

  // État complet courant (envoyé aux nouveaux arrivants) — tenu à jour à chaque rendu.
  const stateRef = useRef({});
  stateRef.current = { mode, slideIdx, slides, strokesMap, bibleBook, bibleChapter, activeVerse, bibleStrong, title };

  // Enregistre, une seule fois, comment fournir et appliquer l'état partagé.
  useEffect(() => {
    // État « léger » envoyé au nouvel arrivant (sans les slides, transmis à part).
    room.setStateProvider(() => {
      const s = stateRef.current;
      return { mode: s.mode, title: s.title, slideIdx: s.slideIdx, strokesMap: s.strokesMap,
        bibleBook: s.bibleBook, bibleChapter: s.bibleChapter, activeVerse: s.activeVerse, bibleStrong: s.bibleStrong };
    });
    // À l'arrivée d'un pair, l'hôte lui envoie les slides un par un (flux ciblé).
    room.setPeerJoinHandler((peerId) => {
      const sl = stateRef.current.slides || [];
      if (!sl.length) return;
      room.sendSyncTo(peerId, { kind: 'slides-begin', total: sl.length, idx: stateRef.current.slideIdx || 0 });
      sl.forEach(slide => room.sendSyncTo(peerId, { kind: 'slide-append', slide, total: sl.length }));
    });
    room.setSyncHandler((msg) => {
      if (!msg) return;
      switch (msg.kind) {
        case 'mode':    setMode(msg.mode); break;
        case 'title':   setTitle(msg.title || ''); break;
        case 'slide':   setSlideIdx(msg.idx || 0); break;
        case 'draw':    applyDrawOp(msg.key, msg.op || {}); break;
        case 'slides-clear': setSlides([]); setSlideIdx(0); setSlidesRecv(null); break;
        case 'slides-begin': setSlides([]); if (msg.idx != null) setSlideIdx(msg.idx);
          setSlidesRecv((msg.total || 0) > 0 ? { have: 0, total: msg.total } : null); break;
        case 'slide-append':
          setSlides(prev => {
            const all = [...prev, msg.slide];
            setSlidesRecv(all.length >= (msg.total || all.length) ? null : { have: all.length, total: msg.total || 0 });
            return all;
          });
          break;
        case 'bible':   setBibleBook(msg.book); setBibleChapter(msg.chapter); setActiveVerse(msg.verse || 1); setBibleStrong(null); break;
        case 'verse':   setActiveVerse(msg.verse); break;
        case 'strong':  setBibleStrong(msg.strong || null); break;
        case 'snapshot': {
          const s = msg.state || {};
          if (s.mode) setMode(s.mode);
          if (s.title != null) setTitle(s.title);
          setSlideIdx(s.slideIdx || 0);   // les slides eux-mêmes suivent (flux dédié)
          setStrokesMap(s.strokesMap || {});
          if (s.bibleBook) setBibleBook(s.bibleBook);
          if (s.bibleChapter) setBibleChapter(s.bibleChapter);
          setActiveVerse(s.activeVerse || 1);
          setBibleStrong(s.bibleStrong || null);
          break;
        }
      }
    });
  }, []);

  return (
    <div className={'app' + (panelOpen ? '' : ' panel-closed')}>
      <div ref={stageRef} className={'stagewrap' + (idle && !panelOpen ? ' idle' : '') + (dragOver ? ' dragover' : '')}
        onDragOver={(e) => { if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files')) { e.preventDefault(); setDragOver(true); } }}
        onDragLeave={(e) => { if (e.target === e.currentTarget) setDragOver(false); }}
        onDrop={(e) => { e.preventDefault(); setDragOver(false); const f = e.dataTransfer.files; if (f && f.length) { changeMode('slides'); importSlides(f); } }}>
        <Toolbar
          mode={mode} setMode={changeMode}
          slides={slides} slideIdx={slideIdx} nextSlide={nextSlide} prevSlide={prevSlide}
          draw={draw} setDraw={setDraw} room={room}
          bibleBook={bibleBook} bibleChapter={bibleChapter} goToRef={goToRef}
          recording={recording} startRec={startRec} stopRec={stopRec}
          panelOpen={panelOpen} setPanelOpen={setPanelOpen}
          installPrompt={installPrompt} triggerInstall={triggerInstall}
        />
        <div className="stage" style={{ width: 1920, height: 1080, transform: `scale(${scale})` }}>
          <WebFrame
            mode={mode} slides={slides} slideIdx={slideIdx}
            camCount={camCount} cams={cams} camShape={camShape} mirror={mirror} title={title}
            roomTiles={roomTiles}
            strokes={strokes} draw={draw}
            drawLive={drawLive} drawCommit={drawCommit} drawRemove={drawRemove} drawAuthor={myDrawId.current}
            bibleBook={bibleBook} bibleChapter={bibleChapter}
            activeVerse={activeVerse} selectVerse={selectVerse}
            bibleStrong={bibleStrong} changeStrong={changeStrong} presRight={presRight}
          />
        </div>
        {draw.active && (
          <DrawingToolbar
            state={draw} setState={setDraw} strokes={strokes}
            onUndo={drawUndo}
            onClearVerse={drawClear}
            onClose={() => setDraw(s => ({ ...s, active: false }))}
          />
        )}
        {dragOver && (
          <div style={{ position: 'absolute', inset: 0, zIndex: 200, background: 'rgba(0,51,51,0.55)',
            display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none' }}>
            <div style={{ color: '#fff', fontFamily: '"Lobster", serif', fontSize: 34,
              border: '3px dashed rgba(251,176,59,0.85)', borderRadius: 18, padding: '28px 48px' }}>
              Déposez vos slides (PDF ou images)
            </div>
          </div>
        )}
        {slidesRecv && (
          <div style={{ position: 'absolute', top: 64, left: '50%', transform: 'translateX(-50%)', zIndex: 150,
            background: 'rgba(0,30,28,0.92)', border: '1px solid rgba(251,176,59,0.45)', borderRadius: 10,
            color: '#fff', padding: '8px 16px', fontSize: 13, letterSpacing: '0.04em',
            boxShadow: '0 10px 30px rgba(0,0,0,0.35)' }}>
            Réception des slides… {slidesRecv.have} / {slidesRecv.total}
          </div>
        )}
      </div>

      {panelOpen && (
        <SidePanel
          camCount={camCount} setCamCount={setCamCount}
          cams={cams} setCams={setCams} camShape={camShape} setCamShape={setCamShape}
          mirror={mirror} setMirror={setMirror}
          title={title} setTitle={changeTitle} room={room}
          mode={mode}
          slides={slides} importing={importing} importSlides={importSlides} clearSlides={clearSlides}
          bibleBook={bibleBook} setBibleBook={changeBook} bibleChapter={bibleChapter} setBibleChapter={changeChapter}
          bibleReady={bibleReady} goToRef={goToRef}
          recording={recording}
          onClose={() => setPanelOpen(false)}
        />
      )}
    </div>
  );
}

// ===================== PLATEAU 1920×1080 =====================
function WebFrame({ mode, slides, slideIdx, camCount, cams, camShape, mirror, title, roomTiles, strokes, draw, drawLive, drawCommit, drawRemove, drawAuthor, bibleBook, bibleChapter, activeVerse, selectVerse, bibleStrong, changeStrong, presRight }) {
  return (
    <div style={{ width: 1920, height: 1080, position: 'relative', overflow: 'hidden',
      background: '#003333', fontFamily: '"Noto Sans", sans-serif' }}>
      <div style={{ position: 'absolute', inset: 0,
        background: 'radial-gradient(85% 95% at 50% 38%, #0a4a47 0%, #003333 55%, #001e1e 100%)' }} />
      <div style={{ position: 'absolute', inset: 0,
        background: 'radial-gradient(45% 40% at 80% 16%, rgba(251,176,59,0.10) 0%, rgba(251,176,59,0) 70%)' }} />

      {/* En-tête */}
      <div style={{ position: 'absolute', top: 30, left: 48, right: 48, zIndex: 5,
        display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
          <img src="assets/logo-yehi-or-blanc.png" style={{ height: 38, opacity: 0.95 }} />
          <div style={{ borderLeft: '1px solid rgba(255,255,255,0.28)', paddingLeft: 14, color: '#fff' }}>
            <div style={{ fontSize: 11, letterSpacing: '0.35em', color: 'rgba(255,255,255,0.6)', textTransform: 'uppercase' }}>Yéhi-Or Institut</div>
            <div style={{ fontFamily: '"Lobster", serif', fontSize: 24, lineHeight: 1.15 }}>{title && title.trim() ? title : 'Présentation'}</div>
          </div>
        </div>
        {mode === 'slides' && slides.length > 0 && (
          <div style={{ color: 'rgba(255,255,255,0.75)', fontSize: 13, letterSpacing: '0.12em' }}>
            {slideIdx + 1} / {slides.length}
          </div>
        )}
      </div>

      {/* Zone de présentation */}
      <div style={{ position: 'absolute', top: 96, left: 48, right: presRight, bottom: 56 }}>
        <div style={{ position: 'absolute', inset: 0, borderRadius: 16, overflow: 'hidden',
          background: mode === 'board' ? '#ffffff' : (mode === 'bible' ? '#fcf8f1' : 'transparent'),
          boxShadow: mode !== 'slides' ? '0 24px 60px rgba(0,0,0,0.30)' : 'none',
          border: mode !== 'slides' ? '1px solid rgba(0,0,0,0.06)' : '0' }}>
          {mode === 'slides' && <SlidesView slides={slides} idx={slideIdx} />}
          {mode === 'bible'  && <BibleBoard book={bibleBook} chapter={bibleChapter} active={activeVerse}
            onSelectVerse={selectVerse} strong={bibleStrong} onStrong={changeStrong} />}
          {/* mode 'board' : surface vierge */}
          <DrawingLayer active={!!draw.active} strokes={strokes}
            tool={draw.tool} color={draw.color} width={draw.width}
            onLive={drawLive} onCommit={drawCommit} onRemove={drawRemove} author={drawAuthor} />
        </div>
      </div>

      {/* Colonne caméras */}
      {(roomTiles ? roomTiles.length > 0 : camCount > 0) &&
        <CameraColumn count={roomTiles ? roomTiles.length : camCount} cams={cams} shape={camShape}
          mirror={mirror} roomTiles={roomTiles} />}
    </div>
  );
}

// ===================== CAMÉRAS =====================
function CameraColumn({ count, cams, shape, mirror, roomTiles }) {
  const top = 96, bottom = 56, right = 48, width = 360, gap = 18;
  const availH = 1080 - top - bottom;
  const twoH = (availH - gap) / 2;            // hauteur d'une tuile quand il y en a 2
  const tileH = Math.min((availH - (count - 1) * gap) / count, twoH); // 1 caméra = même taille que 2
  const radius = shape === 'circle' ? '50%' : 18;
  return (
    <div style={{ position: 'absolute', top, right, width, bottom, display: 'flex', flexDirection: 'column', gap }}>
      {Array.from({ length: count }).map((_, i) => {
        const c = cams[i] || { source: 'placeholder' };
        // Cercle : on force un carré centré pour rester rond
        const tileStyle = { position: 'relative', width: '100%', height: tileH,
          borderRadius: radius, overflow: 'hidden',
          border: '1px solid rgba(251,176,59,0.55)', boxShadow: '0 14px 36px rgba(0,0,0,0.35)' };
        const rt = roomTiles ? roomTiles[i] : null;
        return (
          <div key={rt ? rt.id : i} style={tileStyle}>
            {rt
              ? <StreamTile stream={rt.stream} name={rt.name} muted={rt.muted} mirror={rt.mirror} />
              : c.source === 'local'
              ? <CameraView enabled={true} mode="webcam" deviceId={c.deviceId}
                  mirror={mirror} shape={shape === 'circle' ? 'circle' : 'rounded'}
                  bgPreset={c.bgPreset || 'none'} bgImageDataUrl={null} />
              : c.source === 'vdo'
              ? <VdoTile url={c.vdoId ? `https://vdo.ninja/?view=${c.vdoId}` : c.url} />
              : <PlaceholderTile index={i} />}
          </div>
        );
      })}
    </div>
  );
}

function PlaceholderTile({ index }) {
  return (
    <div style={{ position: 'absolute', inset: 0,
      background: 'radial-gradient(75% 75% at 50% 40%, #0a4a47 0%, #002a27 60%, #001e1e 100%)',
      display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
      gap: 10, color: 'rgba(255,255,255,0.55)', textAlign: 'center', padding: 16 }}>
      <img src="assets/icone-yehi-or.png" style={{ height: '32%', maxHeight: 80, opacity: 0.45 }} />
      <div style={{ fontSize: 12, letterSpacing: '0.22em', textTransform: 'uppercase' }}>Emplacement {index + 1}</div>
      <div style={{ fontSize: 10.5, opacity: 0.75, lineHeight: 1.4 }}>Incrustation OBS</div>
    </div>
  );
}

function VdoTile({ url }) {
  if (!url) return <PlaceholderTile index={0} />;
  let src = url.trim();
  try {
    const u = new URL(src);
    if (!u.searchParams.has('transparent')) u.searchParams.set('transparent', '1');
    if (!u.searchParams.has('cleanoutput')) u.searchParams.set('cleanoutput', '1');
    if (!u.searchParams.has('cover')) u.searchParams.set('cover', '1');
    src = u.toString();
  } catch (e) {}
  return (
    <iframe
      src={src}
      allow="camera;microphone;autoplay;fullscreen;display-capture;picture-in-picture"
      allowFullScreen
      style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', border: 0, background: '#000' }}
    />
  );
}

function randomVdoId() {
  return Math.random().toString(36).slice(2, 10) + Math.random().toString(36).slice(2, 6);
}

// ===================== MODE LECTURE =====================
function LectureBoard({ lesson, active }) {
  const verses = lesson.verses || [];
  return (
    <div style={{ position: 'absolute', inset: 0, padding: '44px 60px', color: '#003333',
      display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
        borderBottom: '1px solid rgba(0,51,51,0.2)', paddingBottom: 14, marginBottom: 6 }}>
        <div style={{ fontSize: 11, letterSpacing: '0.3em', color: '#9c948a', textTransform: 'uppercase' }}>
          {lesson.series || 'Lecture'}
        </div>
        <div style={{ fontFamily: '"Lobster", serif', fontSize: 26, color: '#003333' }}>{lesson.reference || ''}</div>
      </div>
      <div style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden' }}>
        {verses.map((v, i) => <LectureVerse key={v.id || i} v={v} active={i === active} />)}
      </div>
    </div>
  );
}

function LectureVerse({ v, active }) {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 40, padding: '20px 0',
      borderBottom: '1px solid rgba(0,51,51,0.1)', position: 'relative',
      opacity: active ? 1 : 0.4, transition: 'opacity 250ms' }}>
      <div style={{ position: 'absolute', left: '50%', top: 26, transform: 'translateX(-50%)',
        width: 40, height: 40, borderRadius: '50%', background: active ? '#fbb03b' : '#fff',
        border: '1px solid rgba(0,51,51,0.25)', color: active ? '#fff' : '#003333',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        fontFamily: '"Lobster", serif', fontSize: 18, zIndex: 2 }}>{v.n}</div>
      <div style={{ fontStyle: 'italic', fontSize: 21, lineHeight: 1.6, color: '#003333',
        paddingRight: 30, alignSelf: 'center' }}>« {v.fr} »</div>
      <div dir="rtl" style={{ fontFamily: '"Times New Roman", "Frank Ruhl Libre", "Noto Serif Hebrew", serif',
        fontSize: 42, lineHeight: 1.6, color: '#003333', textAlign: 'right', paddingRight: 30 }}>{v.heb}</div>
    </div>
  );
}

// ===================== BARRE D'OUTILS =====================
function BibleSearchBox({ goToRef }) {
  const [q, setQ] = useState('');
  const go = () => {
    const r = (typeof parseBibleSearch === 'function') ? parseBibleSearch(q) : null;
    if (r) { goToRef(r.osis, r.chapter, r.verse); setQ(''); }
    else if (q.trim()) alert('Référence non reconnue. Essaie « Jean 3:16 » ou « Genèse 1 ».');
  };
  return <input className="tb-search" value={q} onChange={e => setQ(e.target.value)}
    onKeyDown={e => { if (e.key === 'Enter') go(); }} placeholder="Aller à… (Jean 3:16)" />;
}

function Toolbar({ mode, setMode, slides, slideIdx, nextSlide, prevSlide,
  draw, setDraw, room, bibleBook, bibleChapter, goToRef, recording, startRec, stopRec, panelOpen, setPanelOpen, installPrompt, triggerInstall }) {
  return (
    <div className="tb-bar">
      <div className="tb-group">
        {MODES.map(m => (
          <button key={m.id} className={'tb-btn' + (mode === m.id ? ' on' : '')}
            onClick={() => setMode(m.id)}>{m.label}</button>
        ))}
      </div>

      {mode === 'slides' && (
        <div className="tb-group">
          <button className="tb-btn" onClick={prevSlide} disabled={slideIdx <= 0} title="Précédent (←)">◀</button>
          <button className="tb-btn" onClick={nextSlide} disabled={slideIdx >= slides.length - 1} title="Suivant (→)">▶</button>
        </div>
      )}

      {mode === 'bible' && (
        <div className="tb-group">
          <button className="tb-btn" title="Chapitre précédent"
            onClick={() => goToRef(bibleBook, Math.max(1, bibleChapter - 1), 1)}>◀</button>
          <BibleSearchBox goToRef={goToRef} />
          <button className="tb-btn" title="Chapitre suivant"
            onClick={() => { const mx = (window.BIBLE_INDEX && window.BIBLE_INDEX[bibleBook] && window.BIBLE_INDEX[bibleBook].chapters) || 999; goToRef(bibleBook, Math.min(mx, bibleChapter + 1), 1); }}>▶</button>
        </div>
      )}

      {room.role && (
        <div className="tb-group">
          <button className={'tb-btn' + (room.micOn ? '' : ' tb-off')} onClick={room.toggleMic}
            title={room.micOn ? 'Couper mon micro' : 'Activer mon micro'}>{room.micOn ? '🎤' : '🔇'}</button>
          <button className={'tb-btn' + (room.camOn ? '' : ' tb-off')} onClick={room.toggleCam}
            title={room.camOn ? 'Couper ma caméra' : 'Activer ma caméra'}>{room.camOn ? '📷' : '🚫'}</button>
        </div>
      )}

      <div className="tb-group">
        <button className={'tb-btn' + (draw.active ? ' on' : '')}
          onClick={() => setDraw(s => ({ ...s, active: !s.active }))} title="Stylet (D)">✎ Stylet</button>
        <button className={'tb-btn' + (recording ? ' tb-rec-on' : '')}
          onClick={recording ? stopRec : startRec} title="Enregistrer la séance">
          {recording ? '⏹ Arrêter' : '⏺ Enregistrer'}
        </button>
        <button className="tb-btn" onClick={() => (document.fullscreenElement ? document.exitFullscreen() : document.documentElement.requestFullscreen())} title="Plein écran (F)">⛶</button>
        {installPrompt && <button className="tb-btn" onClick={triggerInstall} title="Installer l'application">⬇ Installer</button>}
        <button className={'tb-btn' + (panelOpen ? ' on' : '')} onClick={() => setPanelOpen(o => !o)} title="Réglages (E)">⚙ Réglages</button>
      </div>
    </div>
  );
}

// ===================== PANNEAU RÉGLAGES =====================
function SidePanel({ camCount, setCamCount, cams, setCams, camShape, setCamShape, mirror, setMirror,
  title, setTitle, room, mode, slides, importing, importSlides, clearSlides,
  bibleBook, setBibleBook, bibleChapter, setBibleChapter, bibleReady, goToRef, recording, onClose }) {
  const fileRef = useRef(null);
  const setCam = (i, patch) => setCams(cs => cs.map((c, j) => j === i ? { ...c, ...patch } : c));

  return (
    <aside className="sidepanel">
      <div className="sp-head">
        <div>
          <div className="sp-eyebrow">Outil de présentation</div>
          <div className="sp-title">Réglages</div>
        </div>
        <button className="sp-close" onClick={onClose}>Fermer ↗</button>
      </div>

      {/* Présentation */}
      <div className="sp-section">
        <div className="sp-sec-title">Présentation</div>
        <div className="sp-row col">
          <span className="sp-lbl">Titre (haut à gauche du tableau)</span>
          <input className="sp-input" value={title} onChange={e => setTitle(e.target.value)} placeholder="ex. Le webinaire de ce soir" />
        </div>
      </div>

      <RoomSection room={room} />

      {/* Caméras */}
      <div className="sp-section">
        <div className="sp-sec-title">Caméras ({camCount})</div>
        <div className="sp-row">
          <span className="sp-lbl">Nombre</span>
          <div style={{ display: 'flex', gap: 6 }}>
            {[0, 1, 2, 3].map(n => (
              <button key={n} className={'sp-pill' + (camCount === n ? ' on' : '')} onClick={() => setCamCount(n)}>{n}</button>
            ))}
          </div>
        </div>
        {camCount > 0 && (
          <>
            <div className="sp-row">
              <span className="sp-lbl">Forme</span>
              <select className="sp-select" value={camShape} onChange={e => setCamShape(e.target.value)}>
                <option value="rounded">Arrondi</option>
                <option value="circle">Cercle</option>
              </select>
            </div>
            <div className="sp-row">
              <span className="sp-lbl">Miroir (webcam locale)</span>
              <button className={'sp-toggle' + (mirror ? ' on' : '')} onClick={() => setMirror(v => !v)}>{mirror ? 'Activé' : 'Désactivé'}</button>
            </div>
            {Array.from({ length: camCount }).map((_, i) => {
              const c = cams[i] || { source: 'placeholder' };
              return (
                <div key={i} className="sp-cam">
                  <div className="sp-cam-h">Emplacement {i + 1}</div>
                  <select className="sp-select" value={c.source}
                    onChange={e => {
                      const src = e.target.value;
                      const patch = { source: src };
                      if (src === 'vdo' && !c.vdoId) patch.vdoId = randomVdoId();
                      setCam(i, patch);
                    }}>
                    <option value="local">Webcam locale (cet ordinateur)</option>
                    <option value="vdo">VDO.ninja (prof à distance)</option>
                    <option value="placeholder">Emplacement vide (incrustation OBS)</option>
                  </select>
                  {c.source === 'local' && (
                    <>
                      <CameraDevicePicker deviceId={c.deviceId} onChange={id => setCam(i, { deviceId: id })} />
                      <select className="sp-select" value={c.bgPreset || 'none'} onChange={e => setCam(i, { bgPreset: e.target.value })}>
                        {BG_PRESETS.filter(p => p.id !== 'image').map(p => <option key={p.id} value={p.id}>{p.label}</option>)}
                      </select>
                    </>
                  )}
                  {c.source === 'vdo' && (() => {
                    const vid = c.vdoId || randomVdoId();
                    const pushUrl = `https://vdo.ninja/?push=${vid}&label=Prof`;
                    const viewUrl = `https://vdo.ninja/?view=${vid}&transparent=1&cleanoutput=1`;
                    const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(pushUrl)}&size=160x160&color=002424&bgcolor=faf8f4&margin=6`;
                    const copy = (t) => { try { navigator.clipboard.writeText(t); } catch(e){} };
                    if (!c.vdoId) setTimeout(() => setCam(i, { vdoId: vid }), 0);
                    return (
                      <div style={{ display: 'flex', flexDirection: 'column', gap: 10, padding: '8px 0' }}>
                        {/* QR code + instructions */}
                        <div style={{ display: 'flex', gap: 14, alignItems: 'flex-start' }}>
                          <img src={qrUrl} alt="QR VDO.ninja" width={80} height={80}
                            style={{ borderRadius: 8, border: '1px solid rgba(0,51,51,0.2)', flexShrink: 0 }} />
                          <div style={{ fontSize: 12, lineHeight: 1.5, color: '#003333' }}>
                            <strong>Le prof à distance</strong> scanne ce QR code (ou ouvre le lien) sur son appareil pour activer sa caméra.
                          </div>
                        </div>
                        {/* Lien push */}
                        <div style={{ display: 'flex', gap: 6 }}>
                          <input className="sp-input" readOnly value={pushUrl}
                            onFocus={e => e.target.select()}
                            style={{ fontSize: 10.5, flex: 1 }} />
                          <button className="sp-btn ghost" onClick={() => copy(pushUrl)}>Copier</button>
                        </div>
                        {/* Régénérer */}
                        <button className="sp-btn ghost" style={{ fontSize: 11 }}
                          onClick={() => setCam(i, { vdoId: randomVdoId() })}>
                          ↺ Nouveau code
                        </button>
                        <input type="hidden" onChange={() => setCam(i, { url: viewUrl })} />
                      </div>
                    );
                  })()}
                </div>
              );
            })}
            <div className="sp-help">Pour les <strong>profs à distance</strong>, utilise la section <strong>« Réunion »</strong> ci-dessus : leur caméra apparaît automatiquement ici. (L'« emplacement vide » sert seulement si tu préfères incruster un visage par-dessus avec OBS.)</div>
          </>
        )}
      </div>

      {/* Slides */}
      {mode === 'slides' && (
        <div className="sp-section">
          <div className="sp-sec-title">Slides ({slides.length})</div>
          <input ref={fileRef} type="file" accept=".pdf,image/png,image/jpeg,image/gif,image/webp" multiple
            style={{ display: 'none' }} onChange={e => { importSlides(e.target.files); e.target.value = ''; }} />
          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
            <button className="sp-btn" disabled={importing} onClick={() => fileRef.current.click()}>
              {importing ? 'Import…' : '+ Importer des slides'}
            </button>
            {slides.length > 0 && <button className="sp-btn ghost" onClick={clearSlides}>Tout retirer</button>}
          </div>
          <div className="sp-help">Formats : <strong>PDF</strong> (recommandé) et images <strong>PNG/JPG</strong>. PowerPoint → « Enregistrer sous PDF ». Navigation aux flèches ← / →.</div>
        </div>
      )}

      {/* Bible */}
      {mode === 'bible' && (
        <BibleNav book={bibleBook} setBook={setBibleBook} chapter={bibleChapter} setChapter={setBibleChapter}
          ready={bibleReady} onGo={goToRef} />
      )}

      {/* Enregistrement */}
      <div className="sp-section">
        <div className="sp-sec-title">Enregistrement</div>
        <div className="sp-help">
          Le bouton <strong>⏺ Enregistrer</strong> te laisse <strong>choisir</strong> quoi capturer (cet onglet, une fenêtre, ou tout l'écran). L'audio est capturé depuis l'onglet/le système — toutes les voix sont incluses sans mélange micro pour éviter le phasing. Le fichier <code>.webm</code> se télécharge à l'arrêt.
          {recording && <div style={{ color: '#c0392b', marginTop: 6, fontWeight: 600 }}>● Enregistrement en cours…</div>}
        </div>
      </div>

      <div className="sp-section">
        <div className="sp-sec-title">Raccourcis</div>
        <div className="sp-keys">
          <div><kbd>← →</kbd> Slide / verset</div>
          <div><kbd>D</kbd> Stylet</div>
          <div><kbd>F</kbd> Plein écran</div>
          <div><kbd>E</kbd> Réglages</div>
          <div><kbd>Ctrl+Z</kbd> Annuler trait</div>
        </div>
      </div>
    </aside>
  );
}

// ===================== ÉDITEUR DE LECTURE =====================
function LectureEditor({ lesson, setLesson, activeVerse, setActiveVerse }) {
  const set = (patch) => setLesson(L => ({ ...L, ...patch }));
  const setVerse = (i, patch) => setLesson(L => ({ ...L, verses: L.verses.map((v, j) => j === i ? { ...v, ...patch } : v) }));
  const addVerse = () => setLesson(L => {
    const n = L.verses.length ? (L.verses[L.verses.length - 1].n || L.verses.length) + 1 : 1;
    return { ...L, verses: [...L.verses, { id: uid(), n, heb: '', fr: '', annotations: [] }] };
  });
  const removeVerse = (i) => setLesson(L => L.verses.length <= 1 ? L : ({ ...L, verses: L.verses.filter((_, j) => j !== i) }));

  return (
    <div className="sp-section">
      <div className="sp-sec-title">Lecture — versets</div>
      <div className="sp-row col">
        <span className="sp-lbl">Référence</span>
        <input className="sp-input" value={lesson.reference || ''} onChange={e => set({ reference: e.target.value })} placeholder="Genèse 1 · 1–3" />
      </div>
      <div className="sp-row col">
        <span className="sp-lbl">Série / titre</span>
        <input className="sp-input" value={lesson.series || ''} onChange={e => set({ series: e.target.value })} placeholder="Atelier de lecture" />
      </div>
      {lesson.verses.map((v, i) => (
        <div key={v.id || i} className={'sp-verse' + (i === activeVerse ? ' on' : '')}>
          <div className="sp-verse-h">
            <button className="sp-num" onClick={() => setActiveVerse(i)} title="Afficher ce verset">{v.n}</button>
            <input className="sp-input n" type="number" value={v.n} onChange={e => setVerse(i, { n: parseInt(e.target.value, 10) || 1 })} />
            {lesson.verses.length > 1 && <button className="sp-x" onClick={() => removeVerse(i)}>✕</button>}
          </div>
          <textarea className="sp-input heb" dir="rtl" rows={2} value={v.heb} onChange={e => setVerse(i, { heb: e.target.value })} placeholder="Texte hébreu" />
          <textarea className="sp-input" rows={2} value={v.fr} onChange={e => setVerse(i, { fr: e.target.value })} placeholder="Traduction française" />
        </div>
      ))}
      <button className="sp-btn" onClick={addVerse}>+ Verset</button>
    </div>
  );
}

Object.assign(window, { WebinaireApp });
