/* MY GENIE — Geni Voice Live (v1)
 *
 * A live-voice mode for the Geni panel:
 *   • GeniVoiceOrb — the thinking-orbs large preset (geni-orbs.jsx), mapped
 *     onto the session states and paced by the live mic level.
 *   • GeniVoice — the session: mic → speech recognition (Web Speech API)
 *     → onAsk() (the panel's Gemini pipeline) → speech synthesis → listen
 *     again. Graceful error states for denied mic / unsupported browsers.
 *
 * Props:
 *   <GeniVoice onAsk={(text) => Promise<replyText>} lang="fa-IR" onExit={} />
 */

// ---------- Voice session ------------------------------------------

function GeniVoice({ onAsk, onExchange, lang: langProp, onExit }) {
  const [lang, setLang] = React.useState(langProp || 'fa-IR');
  const [state, setState] = React.useState('idle');   // idle|connecting|listening|thinking|speaking|error
  const [heard, setHeard] = React.useState('');
  const [reply, setReply] = React.useState('');
  const [errorMsg, setErrorMsg] = React.useState('');
  const [live, setLive] = React.useState(false);

  const sessionRef = React.useRef({ active: false });
  const stateRef = React.useRef({ state: 'idle', level: 0 });
  const recogRef = React.useRef(null);
  const micRef = React.useRef({ stream: null, ctx: null, raf: 0 });
  const langRef = React.useRef(lang);
  langRef.current = lang;
  const onExchangeRef = React.useRef(onExchange || function () {});
  onExchangeRef.current = onExchange || onExchangeRef.current;
  const liveRef = React.useRef(false);

  function pushState(s) {
    stateRef.current.state = s;
    setState(s);
  }

  function stopMicMeter() {
    const m = micRef.current;
    if (m.raf) cancelAnimationFrame(m.raf);
    m.raf = 0;
    if (m.stream) m.stream.getTracks().forEach(function (tr) { tr.stop(); });
    if (m.ctx) { try { m.ctx.close(); } catch (e) {} }
    micRef.current = { stream: null, ctx: null, raf: 0 };
  }

  function startMicMeter() {
    navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) {
      if (!sessionRef.current.active) return;
      const Ctx = window.AudioContext || window.webkitAudioContext;
      const ctx = new Ctx();
      const src = ctx.createMediaStreamSource(stream);
      const analyser = ctx.createAnalyser();
      analyser.fftSize = 512;
      src.connect(analyser);
      const buf = new Uint8Array(analyser.fftSize);
      micRef.current.stream = stream;
      micRef.current.ctx = ctx;
      (function meter() {
        if (!sessionRef.current.active) return;
        analyser.getByteTimeDomainData(buf);
        let sum = 0;
        for (let i = 0; i < buf.length; i++) {
          const v = (buf[i] - 128) / 128;
          sum += v * v;
        }
        const rms = Math.sqrt(sum / buf.length);
        stateRef.current.level = Math.min(1, rms * 3.2);
        micRef.current.raf = requestAnimationFrame(meter);
      })();
    }).catch(function (e) {
      sessionRef.current.active = false;
      pushState('error');
      setErrorMsg('Microphone access was blocked. Allow it in the browser bar, then restart the session.');
    });
  }

  function startListening() {
    if (!sessionRef.current.active) return;
    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SR) {
      pushState('error');
      setErrorMsg('This browser has no speech recognition. Try Chrome or Edge.');
      return;
    }
    const recog = new SR();
    recog.lang = langRef.current;
    recog.continuous = false;
    recog.interimResults = true;
    recog.maxAlternatives = 1;

    recog.onstart = function () {
      setHeard('');
      startMicMeter();
      pushState('listening');
    };
    recog.onresult = function (ev) {
      let interim = '';
      let final = '';
      for (let i = ev.resultIndex; i < ev.results.length; i++) {
        if (ev.results[i].isFinal) final += ev.results[i][0].transcript;
        else interim += ev.results[i][0].transcript;
      }
      setHeard(final || interim);
      if (final.trim()) {
        recog.stop();
        handleTurn(final.trim());
      }
    };
    recog.onerror = function (ev) {
      if (ev.error === 'not-allowed' || ev.error === 'service-not-allowed') {
        sessionRef.current.active = false;
        stopMicMeter();
        pushState('error');
        setErrorMsg('Speech recognition was blocked. Allow microphone access, then restart.');
      } else if (ev.error !== 'no-speech' && ev.error !== 'aborted') {
        setHeard('');
      }
    };
    recog.onend = function () {
      // Natural no-speech end: resume the loop unless a turn took over.
      if (sessionRef.current.active && stateRef.current.state === 'listening') {
        try { recog.start(); } catch (e) {}
      }
    };

    try { recog.start(); } catch (e) {}
    recogRef.current = recog;
  }

  function handleTurn(text) {
    if (!sessionRef.current.active) return;
    stopMicMeter();
    if (recogRef.current) { try { recogRef.current.abort(); } catch (e) {} }
    setHeard(text);
    pushState('thinking');

    Promise.resolve()
      .then(function () { return onAsk(text); })
      .then(function (replyText) {
        if (!sessionRef.current.active) return;
        const clean = String(replyText || '…')
          .replace(/\*\*/g, '')
          .replace(/\*\([^)]*\)\*/g, '')
          .trim();
        setReply(clean);
        speak(clean);
      })
      .catch(function (e) {
        if (!sessionRef.current.active) return;
        pushState('error');
        setErrorMsg('Something went wrong: ' + (e && e.message ? e.message : e));
      });
  }

  function speak(text) {
    pushState('speaking');
    // Synthetic "voice energy" for the orb — TTS output can't be tapped,
    // so we drive a natural-looking envelope while the utterance plays.
    const t0 = performance.now();
    (function fakeLevel() {
      if (stateRef.current.state !== 'speaking') return;
      const t = (performance.now() - t0) / 1000;
      stateRef.current.level = 0.18 + 0.30 * Math.abs(Math.sin(t * 5.2)) * (0.55 + 0.45 * Math.abs(Math.sin(t * 1.63 + 1)));
      requestAnimationFrame(fakeLevel);
    })();

    const u = new SpeechSynthesisUtterance(text);
    u.lang = langRef.current;
    u.rate = 1.02;
    u.pitch = 1.0;
    const voices = window.speechSynthesis.getVoices();
    const pref = voices.filter(function (v) { return v.lang && v.lang.indexOf(langRef.current.slice(0, 2)) === 0; })[0];
    if (pref) u.voice = pref;
    u.onend = function () {
      if (!sessionRef.current.active) { pushState('idle'); return; }
      setReply('');
      startListening();
    };
    u.onerror = function () {
      if (!sessionRef.current.active) { pushState('idle'); return; }
      setReply('');
      startListening();
    };
    window.speechSynthesis.cancel();
    window.speechSynthesis.speak(u);
  }

  // ---- Live (gemini-3.8-live) path ---------------------------------
  // Real two-way audio: mic PCM → Live session → Gemini's native voice.
  // Falls back to the Web Speech path on any failure.

  function liveSystemInstruction() {
    const digest = (window.MYGENI_LLM && window.MYGENI_LLM.contextDigest)
      ? window.MYGENI_LLM.contextDigest()
      : '{}';
    return [
      'You are Geni, the voice copilot inside "My genie" — a Product Operating System for product teams.',
      'This is a LIVE VOICE conversation: your words are spoken aloud, so keep answers SHORT (1–3 sentences), warm and precise.',
      'Ground every fact in the WORKSPACE CONTEXT JSON below; if it is not there, say so plainly.',
      'Always reply in ' + (langRef.current === 'fa-IR' ? 'Persian (Farsi)' : 'English') + ', matching the language the user speaks.',
      'WORKSPACE CONTEXT (JSON): ' + digest,
    ].join(' ');
  }

  function liveOpts() {
    return {
      apiKey: window.MYGENI_LLM.getKey(),
      systemInstruction: liveSystemInstruction(),
      onState: function (s, detail) {
        if (!sessionRef.current.active) return;
        if (s === 'closed') { pushState('idle'); return; }
        pushState(s);
        if (s === 'error') setErrorMsg(detail || 'Live connection failed.');
        else setErrorMsg('');
      },
      onLevel: function (kind, v) {
        if (kind === 'mic' && stateRef.current.state === 'listening') stateRef.current.level = v;
        if (kind === 'voice' && stateRef.current.state === 'speaking') stateRef.current.level = v;
      },
      onUserText: function (t) { setHeard(t); },
      onModelText: function (t) { setReply(t); },
      onTurnComplete: function (userText, modelText) {
        if (userText || modelText) onExchangeRef.current(userText, modelText);
        setHeard('');
        setReply('');
      },
    };
  }

  function beginLive() {
    setLive(true);
    liveRef.current = true;
    window.GeniLive.start(liveOpts()).catch(function () {
      // Live unavailable (old browser, blocked SDK, dead network) — the
      // Web Speech path keeps the voice session alive.
      if (!sessionRef.current.active) return;
      setLive(false);
      liveRef.current = false;
      startListening();
    });
  }

  function begin() {
    sessionRef.current.active = true;
    setErrorMsg('');
    setReply('');
    setHeard('');
    const key = (window.MYGENI_LLM && window.MYGENI_LLM.getKey) ? window.MYGENI_LLM.getKey() : '';
    if (window.GeniLive && window.GeniLive.supported() && key) beginLive();
    else startListening();
  }

  function stop() {
    sessionRef.current.active = false;
    if (liveRef.current) { liveRef.current = false; setLive(false); window.GeniLive.stop(); }
    stopMicMeter();
    if (recogRef.current) { try { recogRef.current.abort(); } catch (e) {} }
    window.speechSynthesis.cancel();
    stateRef.current.level = 0;
    pushState('idle');
    setHeard('');
    setReply('');
  }

  React.useEffect(function () {
    begin();
    return function () {
      sessionRef.current.active = false;
      if (liveRef.current) { liveRef.current = false; window.GeniLive.stop(); }
      stopMicMeter();
      if (recogRef.current) { try { recogRef.current.abort(); } catch (e) {} }
      window.speechSynthesis.cancel();
    };
  }, []);

  const langMounted = React.useRef(false);
  React.useEffect(function () {
    // Skip the mount run — begin() already opened the session with the
    // right language hint; only react to real language switches.
    if (!langMounted.current) { langMounted.current = true; return; }
    if (liveRef.current) {
      // The language hint lives in the system instruction → restart the
      // Live session so Gemini switches languages cleanly.
      window.GeniLive.stop();
      liveRef.current = false;
      setLive(false);
      if (sessionRef.current.active) beginLive();
      return;
    }
    if (stateRef.current.state === 'listening' && recogRef.current) {
      try { recogRef.current.abort(); } catch (e) {}
    }
  }, [lang]);

  const STATUS = {
    idle: 'Session ended',
    connecting: 'Connecting…',
    listening: 'Listening…',
    thinking: 'Thinking…',
    speaking: 'Speaking…',
    error: 'Voice error',
  };
  const active = state !== 'idle' && state !== 'error';

  return (
    <div className="geni-voice">
      <div className="geni-voice-stage">
        <div className="geni-voice-lang" role="group" aria-label="Recognition language">
          {['fa-IR', 'en-US'].map(function (l) {
            return (
              <button
                key={l}
                className={'geni-voice-lang-btn' + (lang === l ? ' on' : '')}
                onClick={function () { setLang(l); }}
              >
                {l === 'fa-IR' ? 'فا' : 'EN'}
              </button>
            );
          })}
        </div>

        <GeniVoiceOrb stateRef={stateRef} size={224} />

        <div className={'geni-voice-status st-' + state}>
          <span className="geni-voice-status-dot" />
          <span>{(live ? 'LIVE · ' : '') + (STATUS[state] || state)}</span>
        </div>

        {(state === 'listening' || state === 'thinking') && heard && (
          <div className="geni-voice-heard">“{heard}”</div>
        )}
        {state === 'speaking' && reply && (
          <div className="geni-voice-reply">{reply}</div>
        )}
        {state === 'error' && <div className="geni-voice-error">{errorMsg}</div>}

        <div className="geni-voice-actions">
          {active ? (
            <button className="geni-voice-stop" onClick={stop}>
              <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
                <rect x="5" y="5" width="14" height="14" rx="3" />
              </svg>
              <span>End voice</span>
            </button>
          ) : (
            <button className="geni-voice-restart" onClick={begin}>
              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                <path d="M12 3a3 3 0 013 3v5a3 3 0 11-6 0V6a3 3 0 013-3zM5 11a7 7 0 0014 0M12 18v3" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
              </svg>
              <span>Start again</span>
            </button>
          )}
          <button className="geni-voice-chat" onClick={onExit}>
            <span>Back to chat</span>
          </button>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { GeniVoice });
