// VoiceBackend.jsx — simulated "voice generation" backend for Read it for me.
//
// In production this would be a server-side text-to-speech pipeline: each summary
// is sent to a TTS service, the audio is rendered once, stored, and served back
// instantly. This prototype simulates that pipeline so the feature behaves like a
// real product — narration is *pre-generated and persisted* (the browser's
// localStorage stands in for the backend store), and every summary is warmed at
// boot so audio is "ready" the moment a learner opens it, rather than being
// synthesized live on each play.
//
// The stored asset is the prepared narration package: the ordered utterance units
// (opening line + each summary block split into sentences), the narrators it was
// rendered for, a word count and an estimated duration. Playback (ReadAloud.jsx)
// consumes this stored package, so what you hear is exactly what was generated.

(function () {
  const STORE_KEY = 'pl-voice-backend-v1';
  const VOICES = ['ellen', 'allen'];     // the two narrators we pre-render for
  const WORDS_PER_SEC = 2.6;             // ~155 wpm natural narration, for duration estimates

  // ── tiny event bus so UI can reflect generation progress ───────────────────
  const subs = new Set();
  const emit = (evt) => subs.forEach((fn) => { try { fn(evt); } catch (e) {} });

  // ── persistence (the "backend") ────────────────────────────────────────────
  let store = load();
  function load() {
    try {
      const raw = JSON.parse(localStorage.getItem(STORE_KEY) || 'null');
      if (raw && raw.assets) return raw;
    } catch (e) {}
    return { version: 1, assets: {} };
  }
  function save() {
    try { localStorage.setItem(STORE_KEY, JSON.stringify(store)); } catch (e) {}
  }

  // ── content helpers ────────────────────────────────────────────────────────
  function hashStr(s) {
    let h = 5381;
    for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) | 0;
    return (h >>> 0).toString(36);
  }
  function toSentences(text) {
    if (!text) return [];
    return String(text).replace(/\s+/g, ' ').match(/[^.!?]+[.!?]*/g)
      ?.map((s) => s.trim()).filter(Boolean) || [String(text)];
  }
  // Build the ordered narration units from a summary's blocks. Mirrors the queue
  // the player builds, so a stored asset plays back identically.
  function buildUnits(blocks, topicName) {
    const out = [{ blockKey: 'opening', text: `Okay, let's take this gently. Here's your summary of ${topicName || 'this topic'}, one idea at a time.` }];
    (blocks || []).forEach((b) => toSentences(b.text).forEach((s) => out.push({ blockKey: b.key, text: s })));
    return out;
  }
  function hashFor(blocks, topicName) {
    return hashStr((topicName || '') + '|' + (blocks || []).map((b) => b.key + ':' + b.text).join('|'));
  }

  // Reconstruct a summary material's narration blocks from the data layer, the
  // same way Review.jsx's SummaryView does — so prewarm covers every summary,
  // preset-backed or fallback.
  function blocksForMaterial(m) {
    const T = (window.topicById && window.topicById(m.topic)) ||
      { name: m.title || 'Topic', notes: 0 };
    const preset = (window.SUMMARIES || {})[m.id];
    let doc;
    if (preset) {
      doc = { head: preset.head, intro: preset.intro, secs: (preset.secs || []).map((s) => ({ ...s })) };
    } else {
      doc = {
        head: `${T.name}: the essentials`,
        intro: `This summary distills your notes on ${T.name.toLowerCase()} into the ideas most worth remembering. Each section maps back to a source note, so you can jump to the original when you need depth.`,
        secs: [
          { h: 'Core idea', b: 'A few sentences captured from your notes would appear here — the key claim, an example, and the takeaway you should be able to recall on demand.' },
          { h: 'Why it matters', b: 'A few sentences captured from your notes would appear here — the key claim, an example, and the takeaway you should be able to recall on demand.' },
          { h: 'Common pitfalls', b: 'A few sentences captured from your notes would appear here — the key claim, an example, and the takeaway you should be able to recall on demand.' }
        ]
      };
    }
    const blocks = [
      { key: 'head', text: doc.head },
      { key: 'intro', text: doc.intro },
      ...doc.secs.map((s, i) => ({ key: 'sec' + i, text: s.h + '. ' + s.b }))
    ];
    return { blocks, topicName: T.name };
  }

  // ── core: render one asset ─────────────────────────────────────────────────
  function renderAsset(materialId, blocks, topicName) {
    const units = buildUnits(blocks, topicName);
    const words = units.reduce((n, u) => n + (u.text.trim().split(/\s+/).filter(Boolean).length), 0);
    const voicesMap = {};
    VOICES.forEach((v) => { voicesMap[v] = 'ready'; });
    return {
      hash: hashFor(blocks, topicName),
      topicName: topicName || '',
      units,
      words,
      durationSec: Math.max(1, Math.round(words / WORDS_PER_SEC)),
      voices: voicesMap,
      status: 'ready',
      generatedAt: Date.now()
    };
  }

  // Is the stored asset for this content current?
  function isFresh(materialId, blocks, topicName) {
    const a = store.assets[materialId];
    return !!(a && a.status === 'ready' && a.hash === hashFor(blocks, topicName));
  }

  const pending = {}; // materialId -> Promise, de-dupes concurrent ensure() calls

  // Ensure narration for a material is generated + stored. Resolves with the
  // asset. Cached → resolves immediately; otherwise simulates a short render.
  function ensure(materialId, blocks, topicName) {
    if (isFresh(materialId, blocks, topicName)) return Promise.resolve(store.assets[materialId]);
    if (pending[materialId]) return pending[materialId];
    emit({ type: 'generating', materialId });
    pending[materialId] = new Promise((resolve) => {
      // a brief, content-scaled delay stands in for server render time
      const delay = 140 + Math.min(360, (blocks || []).length * 40);
      setTimeout(() => {
        const asset = renderAsset(materialId, blocks, topicName);
        store.assets[materialId] = asset;
        save();
        delete pending[materialId];
        emit({ type: 'ready', materialId, asset });
        resolve(asset);
      }, delay);
    });
    return pending[materialId];
  }

  function get(materialId) { return store.assets[materialId] || null; }
  function statusOf(materialId, blocks, topicName) {
    if (pending[materialId]) return 'generating';
    if (blocks ? isFresh(materialId, blocks, topicName) : (get(materialId) && get(materialId).status === 'ready')) return 'ready';
    return 'missing';
  }

  // List every summary material the app knows about.
  function summaryMaterials() {
    return (window.MATERIALS || []).filter((m) => m.type === 'summary');
  }

  // ── prewarm: generate + store narration for ALL summaries ──────────────────
  let prewarmRun = null;
  function prewarmAll() {
    if (prewarmRun) return prewarmRun;
    const mats = summaryMaterials();
    const total = mats.length;
    let done = 0;
    const tick = (label) => emit({ type: 'progress', ready: done, total, currentLabel: label });
    // count already-cached up front
    mats.forEach((m) => { const { blocks, topicName } = blocksForMaterial(m); if (isFresh(m.id, blocks, topicName)) done++; });
    tick(null);

    const run = (async () => {
      for (const m of mats) {
        const { blocks, topicName } = blocksForMaterial(m);
        if (isFresh(m.id, blocks, topicName)) continue;
        tick(m.title);
        await ensure(m.id, blocks, topicName);
        done++;
        tick(m.title);
      }
      emit({ type: 'done', ready: done, total });
      return { ready: done, total };
    })();
    prewarmRun = run;
    run.finally(() => { if (prewarmRun === run) prewarmRun = null; emit({ type: 'done', ready: done, total }); });
    return prewarmRun;
  }

  function summary() {
    const mats = summaryMaterials();
    let ready = 0;
    mats.forEach((m) => { const { blocks, topicName } = blocksForMaterial(m); if (isFresh(m.id, blocks, topicName)) ready++; });
    return { ready, total: mats.length, generating: Object.keys(pending).length > 0 || !!prewarmRun };
  }

  window.VoiceBackend = {
    VOICES, buildUnits, blocksForMaterial, hashFor,
    ensure, get, statusOf, prewarmAll, summary,
    subscribe: (fn) => { subs.add(fn); return () => subs.delete(fn); },
    clear: () => { store = { version: 1, assets: {} }; save(); }
  };

  // ── React hook for components ──────────────────────────────────────────────
  if (typeof React !== 'undefined') {
    window.useVoiceAsset = function (materialId, blocks, topicName) {
      const [, force] = React.useReducer((x) => x + 1, 0);
      React.useEffect(() => window.VoiceBackend.subscribe((evt) => {
        if (!evt.materialId || evt.materialId === materialId || evt.type === 'done') force();
      }), [materialId]);
      const status = window.VoiceBackend.statusOf(materialId, blocks, topicName);
      const asset = status === 'ready' ? window.VoiceBackend.get(materialId) : null;
      return {
        status, asset,
        ensure: () => window.VoiceBackend.ensure(materialId, blocks, topicName)
      };
    };
  }

  // ── boot: warm the whole voice library once data is available ──────────────
  function boot() {
    if (!(window.MATERIALS && window.MATERIALS.length)) { setTimeout(boot, 60); return; }
    prewarmAll();
  }
  if (document.readyState === 'complete' || document.readyState === 'interactive') setTimeout(boot, 0);
  else document.addEventListener('DOMContentLoaded', () => setTimeout(boot, 0));
})();
