/* ===================================================================
   embed3d.jsx — React → 3D-iframe bridge for the Chat / Rules / Powers drawers

   The 3D world (vrworld/world/embedpanels.js) renders the Chat, Rules and DM
   Powers drawers, but React owns the real data. This headless component mounts
   only while the 3D stage is showing and:

     • posts the resolved chat log, the SRD rules and the DM's element grid into
       the iframe (with boot-time retries to cover the world's async load), and
     • listens for the drawer's actions — a chat line, an element cast, a
       condition clear — and applies them through the real React handlers.

   No-build/global-scope rules: loads before app.jsx, so it uses React.useX
   (not destructured) and reads the leaked globals SRD_SECTIONS / ELEMENTS /
   ELEMENT_TIERS / elementEffect / byId at call time.
   =================================================================== */
function Embed3DBridge({ active, log, sendMsg, isDM, activeRef, order, getEnt, conditions, castElement, clearCondition, roomCode, applyModelBuffer, clearModel, narrate, customFoes, openComposer }) {
  const post = (msg) => { if (window.__embersPostWorld) window.__embersPostWorld(msg); };

  // ---- DM-staged enemies / allies / traps / treasures → 3D standee tokens ----
  // The 3D world iterates EMBERS.ACTORS for its roster / initiative / sonar /
  // visibility, so once these reconcile in, every in-world surface picks them up.
  const actorsData = React.useMemo(() => (customFoes || []).map(f => ({
    id: f.id, name: f.name,
    side: f.allegiance === 'ally' ? 'ally' : 'enemy',     // traps/treasures ride the foe lane (concealed until found)
    sprite: f.sprite || (f.trap ? '🜏' : f.treasure ? '💰' : '👤'),
    color: f.color,
    hp: f.hp, hpMax: f.hpMax, ac: f.ac, init: f.init,
    speed: (f.sheet && f.sheet.speed) || f.speed || 6,
    reveal: f.reveal,
    note: Array.isArray(f.notes) ? f.notes.join(' · ') : (f.note || ''),
    trap: !!f.trap, treasure: !!f.treasure, boss: !!f.boss, kind: f.kind,
    scale: +f.scale || 1,
    modelUrl: f.modelUrl || null, modelName: f.modelName || null, modelScale: +f.modelScale || 1,
    custom: true,
  })), [customFoes]);

  // ---- build the payloads from live React state ----------------------------
  const chatEntries = React.useMemo(() => {
    const idOf = (typeof byId !== 'undefined') ? byId : ((x) => ({ name: '?', color: '#888', sprite: '•' }));
    return (log || [])
      .filter(m => !(m.kind === 'whisper' && !isDM && m.who !== activeRef))
      .map(m => { const w = idOf(m.who) || {}; return {
        id: m.id, kind: m.kind, text: m.text, formula: m.formula, total: m.total,
        who: { name: w.name, color: w.color, sprite: w.sprite } }; });
  }, [log, isDM, activeRef]);

  const powersData = React.useMemo(() => {
    const els = (typeof ELEMENTS !== 'undefined') ? ELEMENTS : [];
    const effOf = (typeof elementEffect !== 'undefined') ? elementEffect : (() => '');
    const tierList = (typeof ELEMENT_TIERS !== 'undefined') ? ELEMENT_TIERS : [];
    const elems = els.map(el => ({
      id: el.id, name: el.name, glyph: el.glyph, color: el.color, kind: el.kind, tier: el.tier,
      effect: effOf(el) }));
    const tiers = tierList.map(t => ({ key: t.key, label: t.label }));
    const targets = (order || []).map(ref => { const e = getEnt(ref) || {}; return {
      ref, name: e.char ? e.char.split(' ')[0] : e.name, sprite: e.portrait || e.sprite,
      color: e.color, hp: e.hp, hpMax: e.hpMax }; });
    const conds = {};
    (order || []).forEach(ref => { const c = conditions && conditions[ref];
      if (c && c.length) conds[ref] = c.map(x => ({ id: x.id, name: x.name, glyph: x.glyph, color: x.color, kind: x.kind })); });
    return { tiers, elements: elems, targets, conditions: conds, role: isDM ? 'dm' : 'player' };
  }, [order, conditions, isDM, getEnt]);

  // conditions for EVERY ref (not just the turn order, and not DM-gated) so the
  // iframe's Initiative tracker + "You" card can show a player their own status.
  const condMap = React.useMemo(() => {
    const out = {};
    Object.keys(conditions || {}).forEach(ref => {
      const c = conditions[ref];
      if (c && c.length) out[ref] = c.map(x => ({ name: x.name, glyph: x.glyph, color: x.color, kind: x.kind }));
    });
    return out;
  }, [conditions]);

  const postAll = React.useCallback(() => {
    post({ type: 'embers:rulesData', sections: (typeof SRD_SECTIONS !== 'undefined') ? SRD_SECTIONS : [] });
    post({ type: 'embers:chatLog', entries: chatEntries, role: isDM ? 'dm' : 'player' });
    post({ type: 'embers:powersData', ...powersData });
    post({ type: 'embers:conditions', key: 'conds', map: condMap });
    post({ type: 'embers:actors', key: 'actors', actors: actorsData });
  }, [chatEntries, powersData, isDM, condMap, actorsData]);

  // push fresh data whenever it changes (only while the 3D stage is live)
  React.useEffect(() => {
    if (!active) return;
    postAll();
    const t1 = setTimeout(postAll, 700);
    const t2 = setTimeout(postAll, 1600);
    return () => { clearTimeout(t1); clearTimeout(t2); };
  }, [active, postAll]);

  // forward the drawer's actions back into the real React handlers
  React.useEffect(() => {
    if (!active) return;
    const onMsg = (e) => {
      const d = e.data; if (!d) return;
      if (d.type === 'embers:reqPanels') postAll();
      else if (d.type === 'embers:chatSend') { if (d.text) sendMsg(d.text); }
      else if (d.type === 'embers:cast') {
        if (!isDM) return;                      // powers are DM-only
        const el = (window.ELEMENTS || []).find(x => x.id === d.elementId);
        // d.shift carries the SHIFT state of the in-iframe click → drop from above
        if (el && d.targetRef) castElement(el, d.targetRef, !!d.shift);
      } else if (d.type === 'embers:clearCond') {
        if (!isDM) return;
        if (d.targetRef && d.condId) clearCondition(d.targetRef, d.condId);
      } else if (d.type === 'embers:uploadModel') {
        // A .glb/.gltf chosen inside the 3D world. The iframe already grafted it
        // locally; persist the bytes here so it survives a world reload + re-post.
        if (d.ref && d.buffer && applyModelBuffer) applyModelBuffer(d.ref, d.buffer, d.name);
      } else if (d.type === 'embers:clearModelReq') {
        if (d.ref && clearModel) clearModel(d.ref);
      } else if (d.type === 'embers:addActor') {
        // The 3D roster's "➕ Add…" button opens the real React composer (the
        // same modal the 2D EncounterTray uses) so the DM stages with full UI.
        if (isDM && openComposer) openComposer();
      } else if (d.type === 'embers:sonarHeard') {
        // The in-world Sonar sweep heard a foe/trap/treasure in range; surface
        // the "you hear…" line in the shared chat as DM narration.
        if (d.text && narrate) narrate(d.text);
      } else if (d.type === 'embers:familiarAsk') {
        // DM-private reasoning assistant. House rules + provider are configured
        // in the 2D React panel; reuse them from the shared-origin localStorage.
        if (!isDM || !window.Familiar) { post({ type: 'embers:familiarReply', ok: false, error: 'The Familiar is DM-only.' }); return; }
        const question = String(d.question || '').trim();
        if (!question) return;
        let houseRules = '', provider = '';
        try {
          houseRules = localStorage.getItem('embers:familiar:houserules:' + (roomCode || 'default')) || '';
          provider = localStorage.getItem('embers:familiar:provider') || '';
        } catch (err) {}
        window.Familiar.ask({ question, history: Array.isArray(d.history) ? d.history : [], houseRules, provider })
          .then(r => post({ type: 'embers:familiarReply', ok: !!(r && r.ok), reply: r && r.reply, error: r && r.reason }))
          .catch(() => post({ type: 'embers:familiarReply', ok: false, error: 'The Familiar could not be reached.' }));
      }
    };
    window.addEventListener('message', onMsg);
    return () => window.removeEventListener('message', onMsg);
  }, [active, postAll, sendMsg, isDM, castElement, clearCondition, roomCode, applyModelBuffer, clearModel, narrate, openComposer]);

  return null;
}
window.Embed3DBridge = Embed3DBridge;
