"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.useXaiRealtimeSession = void 0;
var _dataSources = require("@mirai/data-sources");
var _react = require("react");
var _helpers = require("../../../../helpers");
var _helpers2 = require("../../../helpers");
var _lobby = require("../../../services/lobby.service");
const XAI_REALTIME_URL = 'wss://api.x.ai/v1/realtime';
const DEFAULT_MODEL = 'grok-voice-latest';
const SAMPLE_RATE = 24000;
const PROCESSOR_BUFFER_SIZE = 2048;
const ALLOWED_OPEN_URL_PREFIX = 'https://api.mirai';
const float32ToInt16 = float32 => {
  const int16 = new Int16Array(float32.length);
  for (let i = 0; i < float32.length; i += 1) {
    const sample = Math.max(-1, Math.min(1, float32[i]));
    int16[i] = sample < 0 ? sample * 0x8000 : sample * 0x7fff;
  }
  return int16;
};
const int16ToFloat32 = int16 => {
  const float32 = new Float32Array(int16.length);
  for (let i = 0; i < int16.length; i += 1) {
    float32[i] = int16[i] / 0x8000;
  }
  return float32;
};
const base64FromInt16 = int16 => {
  const bytes = new Uint8Array(int16.buffer, int16.byteOffset, int16.byteLength);
  let binary = '';
  const chunkSize = 0x8000;
  for (let i = 0; i < bytes.length; i += chunkSize) {
    binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize));
  }
  return btoa(binary);
};
const int16FromBase64 = b64 => {
  const binary = atob(b64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
  return new Int16Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 2));
};
const extractAudioDelta = payload => {
  var _payload$delta;
  // xAI docs show response.output_audio.delta with delta: { type: 'audio_delta', audio: 'base64' }.
  // Stay defensive in case the server returns a plain string in delta or a top-level audio field.
  if (typeof (payload === null || payload === void 0 ? void 0 : payload.delta) === 'string') return payload.delta;
  if (typeof (payload === null || payload === void 0 ? void 0 : (_payload$delta = payload.delta) === null || _payload$delta === void 0 ? void 0 : _payload$delta.audio) === 'string') return payload.delta.audio;
  if (typeof (payload === null || payload === void 0 ? void 0 : payload.audio) === 'string') return payload.audio;
  return null;
};
const useXaiRealtimeSession = () => {
  const wsRef = (0, _react.useRef)(null);
  const audioContextRef = (0, _react.useRef)(null);
  const inputSourceRef = (0, _react.useRef)(null);
  const scriptProcessorRef = (0, _react.useRef)(null);
  const inputAnalyserRef = (0, _react.useRef)(null);
  const outputAnalyserRef = (0, _react.useRef)(null);
  const inputMuteGainRef = (0, _react.useRef)(null);
  const localStreamRef = (0, _react.useRef)(null);
  const microphoneEnabledRef = (0, _react.useRef)(false);
  const playbackTimeRef = (0, _react.useRef)(0);
  const activePlaybackSourcesRef = (0, _react.useRef)(new Set());
  const inputFrequencyDataRef = (0, _react.useRef)(null);
  const outputFrequencyDataRef = (0, _react.useRef)(null);
  const audioAnalyzersRef = (0, _react.useRef)({
    inputAnalyser: null,
    outputAnalyser: null,
    getInputByteFrequencyData: () => null,
    getOutputByteFrequencyData: () => null
  });
  const pendingSessionConfigRef = (0, _react.useRef)(null);
  const sessionAppliedRef = (0, _react.useRef)(false);
  const introSentRef = (0, _react.useRef)(false);
  const pendingUserTranscriptRef = (0, _react.useRef)('');
  const handledOpenUrlCallsRef = (0, _react.useRef)(new Set());
  const cleanupSession = (0, _react.useCallback)(() => {
    const ws = wsRef.current;
    if (ws) {
      if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) ws.close();
      wsRef.current = null;
    }
    if (scriptProcessorRef.current) {
      scriptProcessorRef.current.onaudioprocess = null;
      scriptProcessorRef.current.disconnect();
      scriptProcessorRef.current = null;
    }
    if (inputSourceRef.current) {
      inputSourceRef.current.disconnect();
      inputSourceRef.current = null;
    }
    if (inputMuteGainRef.current) {
      inputMuteGainRef.current.disconnect();
      inputMuteGainRef.current = null;
    }
    if (inputAnalyserRef.current) {
      inputAnalyserRef.current.disconnect();
      inputAnalyserRef.current = null;
    }
    if (outputAnalyserRef.current) {
      outputAnalyserRef.current.disconnect();
      outputAnalyserRef.current = null;
    }
    activePlaybackSourcesRef.current.forEach(source => {
      try {
        source.stop();
      } catch (_unused) {
        // already stopped
      }
    });
    activePlaybackSourcesRef.current = new Set();
    if (localStreamRef.current) {
      localStreamRef.current.getTracks().forEach(track => track.stop());
      localStreamRef.current = null;
    }
    if (audioContextRef.current) {
      audioContextRef.current.close();
      audioContextRef.current = null;
    }
    audioAnalyzersRef.current.inputAnalyser = null;
    audioAnalyzersRef.current.outputAnalyser = null;
    audioAnalyzersRef.current.getInputByteFrequencyData = () => null;
    audioAnalyzersRef.current.getOutputByteFrequencyData = () => null;
    inputFrequencyDataRef.current = null;
    outputFrequencyDataRef.current = null;
    microphoneEnabledRef.current = false;
    playbackTimeRef.current = 0;
    pendingSessionConfigRef.current = null;
    sessionAppliedRef.current = false;
    introSentRef.current = false;
    pendingUserTranscriptRef.current = '';
    handledOpenUrlCallsRef.current = new Set();
  }, []);
  const stopPendingPlayback = (0, _react.useCallback)(() => {
    activePlaybackSourcesRef.current.forEach(source => {
      try {
        source.stop();
      } catch (_unused2) {
        // already stopped
      }
    });
    activePlaybackSourcesRef.current = new Set();
    if (audioContextRef.current) playbackTimeRef.current = audioContextRef.current.currentTime;
  }, []);
  const scheduleAudioPlayback = (0, _react.useCallback)(audioBase64 => {
    const context = audioContextRef.current;
    const analyser = outputAnalyserRef.current;
    if (!context || !analyser) return;
    const int16 = int16FromBase64(audioBase64);
    if (!int16.length) return;
    const float32 = int16ToFloat32(int16);
    const buffer = context.createBuffer(1, float32.length, SAMPLE_RATE);
    buffer.copyToChannel(float32, 0);
    const source = context.createBufferSource();
    source.buffer = buffer;
    source.connect(analyser);
    const startAt = Math.max(context.currentTime, playbackTimeRef.current);
    source.start(startAt);
    playbackTimeRef.current = startAt + buffer.duration;
    activePlaybackSourcesRef.current.add(source);
    source.onended = () => {
      activePlaybackSourcesRef.current.delete(source);
    };
  }, []);
  const sendJson = (0, _react.useCallback)(payload => {
    const ws = wsRef.current;
    if (!ws || ws.readyState !== WebSocket.OPEN) return false;
    ws.send(JSON.stringify(payload));
    return true;
  }, []);
  const sendIntro = (0, _react.useCallback)(_ref => {
    let {
      assistantName,
      locale
    } = _ref;
    if (introSentRef.current) return;
    const localeHint = locale || 'en-US';
    const text = "Respond in the language that matches the locale code \"".concat(localeHint, "\". Introduce yourself using the following sentence, translated into that language: \"Hello, I am ").concat(assistantName, ", your virtual assistant. I can help you with information about our services and facilities. What would you like to know today?\"");
    sendJson({
      type: 'conversation.item.create',
      item: {
        type: 'message',
        role: 'user',
        content: [{
          type: 'input_text',
          text
        }]
      }
    });
    sendJson({
      type: 'response.create'
    });
    introSentRef.current = true;
  }, [sendJson]);
  const handleFunctionCall = (0, _react.useCallback)(payload => {
    var _parsed;
    const {
      name
    } = payload;
    const callId = payload.call_id;
    if (name !== 'open_url' || !callId) return;
    if (handledOpenUrlCallsRef.current.has(callId)) return;
    handledOpenUrlCallsRef.current.add(callId);
    let parsed = null;
    try {
      parsed = JSON.parse(payload.arguments || '{}');
    } catch (_unused3) {
      parsed = null;
    }
    const url = (_parsed = parsed) === null || _parsed === void 0 ? void 0 : _parsed.url;
    const allowed = typeof url === 'string' && url.trim().startsWith(ALLOWED_OPEN_URL_PREFIX);
    if (!allowed) {
      sendJson({
        type: 'conversation.item.create',
        item: {
          type: 'function_call_output',
          call_id: callId,
          output: JSON.stringify({
            error: 'blocked_url',
            allowed_prefix: ALLOWED_OPEN_URL_PREFIX
          })
        }
      });
      sendJson({
        type: 'response.create'
      });
      return;
    }
    cleanupSession();
    window.location.assign(url.trim());
  }, [cleanupSession, sendJson]);
  const persistTranscriptPair = (0, _react.useCallback)(async _ref2 => {
    let {
      fingerprint,
      hotelId,
      locale,
      transcript
    } = _ref2;
    if (!transcript || !pendingUserTranscriptRef.current) return;
    const input = pendingUserTranscriptRef.current.trim();
    pendingUserTranscriptRef.current = '';
    const activeLocale = locale || 'en-US';
    if (hotelId) {
      try {
        const chatId = await (0, _lobby.ensureChatId)({
          fingerprint,
          channelId: hotelId,
          session: {
            locale: activeLocale
          }
        });
        if (chatId) {
          await (0, _lobby.createChatMessage)({
            author: 'user',
            body: input,
            chatId,
            meta: {
              audio: true
            }
          });
          await (0, _lobby.createChatMessage)({
            author: 'ai',
            body: transcript,
            chatId,
            meta: {
              audio: true
            }
          });
        }
      } catch (error) {
        _dataSources.Event.publish(_helpers.EVENT.METRICS, {
          id: _helpers.METRICS_EVENTS.AURA_ERROR,
          error: error === null || error === void 0 ? void 0 : error.message
        });
      }
    }
    _dataSources.Event.publish(_helpers.EVENT.METRICS, {
      id: 'SYNAPSE_RESPONSE',
      fingerprint,
      text: transcript,
      input,
      agent: 'aura',
      locale: activeLocale
    });
  }, []);
  const startSession = (0, _react.useCallback)(async _ref3 => {
    let {
      assistant_name,
      chainId,
      currency,
      hotelId,
      isChain,
      locale,
      setIsConnecting,
      setReady
    } = _ref3;
    cleanupSession();
    const fingerprint = (0, _helpers2.getClientFingerprint)();
    const finalizeSession = error => {
      const errorMessage = typeof error === 'string' ? error : error === null || error === void 0 ? void 0 : error.message;
      if (errorMessage) {
        _dataSources.Event.publish(_helpers.EVENT.METRICS, {
          id: _helpers.METRICS_EVENTS.AURA_ERROR,
          error: errorMessage
        });
      }
      cleanupSession();
      setReady(false);
      setIsConnecting(false);
    };
    try {
      const sessionResponse = await (0, _dataSources.request)({
        endpoint: "/voice/session?voiceModel=xai&fingerprint=".concat(fingerprint, "&hotelId=").concat(isChain ? '' : hotelId !== null && hotelId !== void 0 ? hotelId : '', "&chainId=").concat(isChain ? chainId !== null && chainId !== void 0 ? chainId : '' : '', "&locale=").concat(locale !== null && locale !== void 0 ? locale : '', "&currency=").concat(currency !== null && currency !== void 0 ? currency : ''),
        hostname: process.env.SERVICE_SYNAPSE,
        method: 'GET'
      });
      const ephemeralToken = sessionResponse === null || sessionResponse === void 0 ? void 0 : sessionResponse.value;
      const sessionConfig = sessionResponse === null || sessionResponse === void 0 ? void 0 : sessionResponse.session;
      const model = (sessionResponse === null || sessionResponse === void 0 ? void 0 : sessionResponse.model) || DEFAULT_MODEL;
      if (!ephemeralToken || !sessionConfig) throw new Error('Invalid xAI session response');
      pendingSessionConfigRef.current = sessionConfig;
      const AudioContextConstructor = globalThis.AudioContext || globalThis.webkitAudioContext;
      if (!AudioContextConstructor) throw new Error('AudioContext not supported');
      const audioContext = new AudioContextConstructor({
        sampleRate: SAMPLE_RATE
      });
      audioContextRef.current = audioContext;
      if (audioContext.state === 'suspended') await audioContext.resume();
      const mediaStream = await navigator.mediaDevices.getUserMedia({
        audio: {
          echoCancellation: true,
          noiseSuppression: true,
          autoGainControl: true,
          channelCount: 1,
          sampleRate: SAMPLE_RATE
        }
      });
      localStreamRef.current = mediaStream;
      const inputSource = audioContext.createMediaStreamSource(mediaStream);
      inputSourceRef.current = inputSource;
      const inputAnalyser = audioContext.createAnalyser();
      inputAnalyser.fftSize = 1024;
      inputAnalyserRef.current = inputAnalyser;
      inputSource.connect(inputAnalyser);
      const outputAnalyser = audioContext.createAnalyser();
      outputAnalyser.fftSize = 1024;
      outputAnalyserRef.current = outputAnalyser;
      outputAnalyser.connect(audioContext.destination);
      inputFrequencyDataRef.current = new Uint8Array(inputAnalyser.frequencyBinCount);
      outputFrequencyDataRef.current = new Uint8Array(outputAnalyser.frequencyBinCount);
      audioAnalyzersRef.current.inputAnalyser = inputAnalyser;
      audioAnalyzersRef.current.outputAnalyser = outputAnalyser;
      audioAnalyzersRef.current.getInputByteFrequencyData = () => {
        if (!inputAnalyserRef.current || !inputFrequencyDataRef.current) return null;
        inputAnalyserRef.current.getByteFrequencyData(inputFrequencyDataRef.current);
        return inputFrequencyDataRef.current;
      };
      audioAnalyzersRef.current.getOutputByteFrequencyData = () => {
        if (!outputAnalyserRef.current || !outputFrequencyDataRef.current) return null;
        outputAnalyserRef.current.getByteFrequencyData(outputFrequencyDataRef.current);
        return outputFrequencyDataRef.current;
      };

      // ScriptProcessor is deprecated but still the simplest portable way to obtain
      // raw PCM frames synchronously. It must be connected to the destination to be scheduled;
      // we route it through a muted GainNode so the mic input is never audible locally.
      const processor = audioContext.createScriptProcessor(PROCESSOR_BUFFER_SIZE, 1, 1);
      scriptProcessorRef.current = processor;
      const muteGain = audioContext.createGain();
      muteGain.gain.value = 0;
      inputMuteGainRef.current = muteGain;
      inputSource.connect(processor);
      processor.connect(muteGain);
      muteGain.connect(audioContext.destination);
      processor.onaudioprocess = event => {
        if (!microphoneEnabledRef.current) return;
        const ws = wsRef.current;
        if (!ws || ws.readyState !== WebSocket.OPEN) return;
        const channelData = event.inputBuffer.getChannelData(0);
        const int16 = float32ToInt16(channelData);
        ws.send(JSON.stringify({
          type: 'input_audio_buffer.append',
          audio: base64FromInt16(int16)
        }));
      };
      const ws = new WebSocket("".concat(XAI_REALTIME_URL, "?model=").concat(encodeURIComponent(model)), ["xai-client-secret.".concat(ephemeralToken)]);
      wsRef.current = ws;
      ws.addEventListener('open', () => {
        _dataSources.Event.publish(_helpers.EVENT.METRICS, {
          id: _helpers.METRICS_EVENTS.AURA_START
        });
        sendJson({
          type: 'session.update',
          session: pendingSessionConfigRef.current
        });
      });
      ws.addEventListener('error', () => finalizeSession('xAI WebSocket error'));
      ws.addEventListener('close', () => finalizeSession());
      ws.addEventListener('message', async event => {
        let payload;
        try {
          payload = JSON.parse(event.data);
        } catch (_unused4) {
          return;
        }
        switch (payload.type) {
          case 'session.created':
          case 'session.updated':
            {
              if (sessionAppliedRef.current) break;
              sessionAppliedRef.current = true;
              setReady(true);
              sendIntro({
                assistantName: assistant_name,
                locale
              });
              break;
            }
          case 'response.output_audio.delta':
            {
              const audio = extractAudioDelta(payload);
              if (audio) scheduleAudioPlayback(audio);
              break;
            }
          case 'input_audio_buffer.speech_started':
            {
              // Barge-in: cut current playback so the user can take over the turn cleanly.
              stopPendingPlayback();
              break;
            }
          case 'response.function_call_arguments.done':
            {
              handleFunctionCall(payload);
              break;
            }
          case 'response.done':
            {
              // Open the microphone once the intro response finishes; subsequent turns use server VAD.
              if (!microphoneEnabledRef.current) microphoneEnabledRef.current = true;
              break;
            }
          case 'conversation.item.input_audio_transcription.completed':
            {
              var _payload$transcript;
              const transcript = (_payload$transcript = payload.transcript) === null || _payload$transcript === void 0 ? void 0 : _payload$transcript.trim();
              if (transcript) pendingUserTranscriptRef.current += transcript;
              break;
            }
          case 'response.output_audio_transcript.done':
          case 'response.text.done':
            {
              var _ref4;
              const transcript = (_ref4 = payload.transcript || payload.text) === null || _ref4 === void 0 ? void 0 : _ref4.trim();
              await persistTranscriptPair({
                fingerprint,
                hotelId,
                locale,
                transcript
              });
              break;
            }
          default:
            break;
        }
      });
    } catch (error) {
      _dataSources.Event.publish(_helpers.EVENT.METRICS, {
        id: _helpers.METRICS_EVENTS.AURA_ERROR,
        error: error === null || error === void 0 ? void 0 : error.message
      });
      setReady(false);
      throw error;
    } finally {
      setIsConnecting(false);
    }
  }, [cleanupSession, handleFunctionCall, persistTranscriptPair, scheduleAudioPlayback, sendIntro, sendJson, stopPendingPlayback]);
  const endSession = (0, _react.useCallback)(() => {
    cleanupSession();
  }, [cleanupSession]);
  return {
    startSession,
    endSession,
    audioAnalyzers: audioAnalyzersRef.current
  };
};
exports.useXaiRealtimeSession = useXaiRealtimeSession;
//# sourceMappingURL=useXaiRealtimeSession.js.map