/**
 * VMO Realtime Voice Session — WebRTC client for OpenAI Realtime API.
 *
 * Replaces the legacy STT (Web Speech) → text → TTS pipeline with a single
 * full-duplex audio channel. The server-side VAD on OpenAI emits
 * `input_audio_buffer.speech_started` the instant the user starts talking,
 * which we use as the barge-in trigger: we send `response.cancel` and stop
 * the inbound audio sink so the AI shuts up mid-syllable.
 *
 * Public API:
 *   const s = new VMORealtimeSession({ context: 'jarvis', voice: 'ash' });
 *   await s.start();             // mints token, opens mic, starts WebRTC
 *   s.on('transcript.user',  ({text, final}) => ...);
 *   s.on('transcript.assistant', ({text, final}) => ...);
 *   s.on('status', (s) => ...);  // 'connecting' | 'live' | 'speaking' | 'listening' | 'ended' | 'error'
 *   s.on('error', (e) => ...);
 *   s.mute();   s.unmute();
 *   s.stop();
 *
 * Marker: VMO_REALTIME_CLIENT_v1
 */
(function (global) {
    'use strict';

    var REALTIME_URL_BASE = 'https://api.openai.com/v1/realtime';

    function isSupported() {
        return !!(global.RTCPeerConnection &&
                  global.navigator && global.navigator.mediaDevices &&
                  global.navigator.mediaDevices.getUserMedia);
    }

    function ajaxBase() {
        if (global.vmo_ajax && global.vmo_ajax.ajaxurl) return global.vmo_ajax;
        if (global.vmoAjax && global.vmoAjax.ajaxurl)  return global.vmoAjax;
        return { ajaxurl: '/wp-admin/admin-ajax.php', nonce: '' };
    }

    function VMORealtimeSession(opts) {
        opts = opts || {};
        this.context = opts.context || 'jarvis';      // jarvis | consultation | receptionist
        this.voice   = opts.voice   || (global.vmoSelectedVoice || 'ash');
        this.actionToken    = opts.action || 'vmo_realtime_session';
        this.toolAction     = opts.toolAction || 'vmo_realtime_tool';
        this.audioSinkId    = opts.audioSinkId || 'vmo-realtime-audio';
        this.debug          = !!opts.debug;

        this.pc = null;
        this.dc = null;
        this.localStream = null;
        this.audioSink = null;
        this.session = null;            // server response { client_secret, model, ... }
        this.status = 'idle';
        this.muted = !!opts.muted;
        this.activeResponseId = null;
        this._listeners = {};
        this._userTextBuf = '';
        this._asstTextBuf = '';
    }

    VMORealtimeSession.isSupported = isSupported;

    VMORealtimeSession.prototype.on = function (evt, cb) {
        (this._listeners[evt] = this._listeners[evt] || []).push(cb);
        return this;
    };
    VMORealtimeSession.prototype.off = function (evt, cb) {
        var arr = this._listeners[evt]; if (!arr) return this;
        this._listeners[evt] = arr.filter(function (f) { return f !== cb; });
        return this;
    };
    VMORealtimeSession.prototype._emit = function (evt, payload) {
        var arr = this._listeners[evt]; if (!arr) return;
        for (var i = 0; i < arr.length; i++) {
            try { arr[i](payload); } catch (e) { if (this.debug) console.warn('[VMORealtime] listener error', e); }
        }
    };
    VMORealtimeSession.prototype._setStatus = function (s) {
        if (this.status === s) return;
        this.status = s;
        this._emit('status', s);
    };
    VMORealtimeSession.prototype._log = function () {
        if (!this.debug) return;
        try { console.log.apply(console, ['[VMORealtime]'].concat([].slice.call(arguments))); } catch (_) {}
    };

    // --- 1. Mint ephemeral token from the WordPress server -------------------
    VMORealtimeSession.prototype._mintToken = function () {
        var ax = ajaxBase();
        var body = new URLSearchParams();
        body.set('action', this.actionToken);
        body.set('security', ax.nonce || '');
        body.set('nonce',    ax.nonce || '');
        body.set('context',  this.context);
        body.set('voice',    this.voice);

        return fetch(ax.ajaxurl, {
            method: 'POST',
            credentials: 'same-origin',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
            body: body.toString(),
        }).then(function (r) { return r.json(); }).then(function (j) {
            if (!j || !j.success || !j.data || !j.data.client_secret) {
                throw new Error((j && j.data && j.data.message) || 'Failed to mint Realtime token');
            }
            return j.data;
        });
    };

    // --- 2. Open mic and ensure remote audio sink ----------------------------
    VMORealtimeSession.prototype._openMic = function () {
        return global.navigator.mediaDevices.getUserMedia({
            audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
            video: false,
        });
    };

    VMORealtimeSession.prototype._ensureSink = function () {
        var sink = global.document.getElementById(this.audioSinkId);
        if (!sink) {
            sink = global.document.createElement('audio');
            sink.id = this.audioSinkId;
            sink.autoplay = true;
            sink.playsInline = true;
            sink.setAttribute('playsinline', '');
            sink.style.display = 'none';
            global.document.body.appendChild(sink);
        }
        return sink;
    };

    // --- 3. Public start() — full lifecycle ----------------------------------
    VMORealtimeSession.prototype.start = function () {
        var self = this;
        if (!isSupported()) {
            var err = new Error('WebRTC or getUserMedia not supported in this browser');
            this._setStatus('error'); this._emit('error', err);
            return Promise.reject(err);
        }
        if (this.status === 'connecting' || this.status === 'live' ||
            this.status === 'listening' || this.status === 'speaking') {
            return Promise.resolve(this);
        }
        this._setStatus('connecting');

        return this._mintToken().then(function (sess) {
            self.session = sess;
            return self._openMic();
        }).then(function (stream) {
            self.localStream = stream;
            if (self.muted) {
                try { stream.getAudioTracks().forEach(function (t) { t.enabled = false; }); } catch (_) {}
            }
            self.audioSink = self._ensureSink();

            var pc = new global.RTCPeerConnection();
            self.pc = pc;

            // Remote audio
            pc.ontrack = function (ev) {
                try { self.audioSink.srcObject = ev.streams[0]; } catch (_) {
                    self.audioSink.srcObject = new MediaStream([ev.track]);
                }
            };

            // Local mic
            stream.getAudioTracks().forEach(function (t) { pc.addTrack(t, stream); });

            // Data channel for events (must be created before offer)
            var dc = pc.createDataChannel('oai-events');
            self.dc = dc;
            dc.onopen = function () {
                self._log('data channel open');
                self._setStatus('live');
                // Apply the user-selected voice and keep only one intentional wait:
                // a short thinking pause so Jarvis does not cut off the user.
                self._send({
                    type: 'session.update',
                    session: {
                        voice: self.voice,
                        // Barge-in stays high-sensitivity via threshold/padding;
                        // response creation waits for the requested user pause.
                        turn_detection: {
                            type: 'server_vad',
                            threshold: 0.28,
                            prefix_padding_ms: 150,
                            silence_duration_ms: 5500,
                            create_response: true,
                        },
                    },
                });
                self._emit('ready', { sessionId: sess.session_id, model: sess.model });
            };
            dc.onmessage = function (ev) {
                try { self._handleEvent(JSON.parse(ev.data)); }
                catch (e) { self._log('event parse error', e, ev.data); }
            };
            dc.onerror = function (e) { self._log('dc error', e); };
            dc.onclose = function () { self._log('dc closed'); };

            // Offer/answer with OpenAI Realtime endpoint, authenticated by ephemeral key
            return pc.createOffer().then(function (offer) {
                return pc.setLocalDescription(offer).then(function () { return offer; });
            }).then(function (offer) {
                var url = REALTIME_URL_BASE + '?model=' + encodeURIComponent(sess.model);
                return fetch(url, {
                    method: 'POST',
                    body: offer.sdp,
                    headers: {
                        'Authorization': 'Bearer ' + sess.client_secret,
                        'Content-Type': 'application/sdp',
                        'OpenAI-Beta': 'realtime=v1',
                    },
                });
            }).then(function (r) {
                if (!r.ok) { return r.text().then(function (t) { throw new Error('SDP exchange failed: ' + r.status + ' ' + t); }); }
                return r.text();
            }).then(function (answerSdp) {
                return pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });
            });
        }).then(function () {
            return self;
        }).catch(function (err) {
            self._log('start failed', err);
            self._setStatus('error');
            self._emit('error', err);
            self._teardown();
            throw err;
        });
    };

    // --- 4. Send arbitrary event over the data channel -----------------------
    VMORealtimeSession.prototype._send = function (obj) {
        if (!this.dc || this.dc.readyState !== 'open') return false;
        try { this.dc.send(JSON.stringify(obj)); return true; }
        catch (e) { this._log('send failed', e); return false; }
    };

    VMORealtimeSession.prototype.cancelResponse = function () {
        if (this.activeResponseId) {
            this._send({ type: 'response.cancel' });
            this.activeResponseId = null;
        }
        try {
            if (this.audioSink) {
                this.audioSink.pause();
                var srcObj = this.audioSink.srcObject;
                this.audioSink.srcObject = null;
                this.audioSink.srcObject = srcObj;
                var p = this.audioSink.play(); if (p && p.catch) p.catch(function(){});
            }
        } catch (_) {}
        this._setStatus('listening');
        this._emit('barge_in');
    };

    // --- 5. Handle inbound Realtime events -----------------------------------
    VMORealtimeSession.prototype._handleEvent = function (ev) {
        var t = ev && ev.type;
        if (!t) return;
        this._log('evt', t);

        switch (t) {
            // User started speaking → barge-in.
            case 'input_audio_buffer.speech_started':
                this.cancelResponse();
                break;

            case 'input_audio_buffer.speech_stopped':
                // Server VAD will create a response automatically (create_response=true)
                break;

            case 'response.created':
                this.activeResponseId = (ev.response && ev.response.id) || null;
                this._setStatus('speaking');
                this._asstTextBuf = '';
                break;

            case 'response.audio_transcript.delta':
                if (ev.delta) {
                    this._asstTextBuf += ev.delta;
                    this._emit('transcript.assistant', { text: this._asstTextBuf, delta: ev.delta, final: false });
                }
                break;

            case 'response.audio_transcript.done':
                this._emit('transcript.assistant', { text: this._asstTextBuf, delta: '', final: true });
                this._asstTextBuf = '';
                break;

            case 'conversation.item.input_audio_transcription.completed':
                if (ev.transcript) {
                    this._emit('transcript.user', { text: ev.transcript, final: true });
                }
                break;

            case 'response.done':
                this.activeResponseId = null;
                this._setStatus('live');
                this._emit('response_done', ev.response || null);
                break;

            // Function calling — the model wants to invoke a tool.
            case 'response.function_call_arguments.done':
                this._dispatchToolCall(ev);
                break;

            case 'error':
                this._emit('error', new Error((ev.error && ev.error.message) || 'Realtime error'));
                break;
        }
    };

    // --- 6. Tool-call bridge → server → back into conversation ---------------
    VMORealtimeSession.prototype._dispatchToolCall = function (ev) {
        var self = this;
        var name = ev.name;
        var callId = ev.call_id;
        var args = {};
        try { args = JSON.parse(ev.arguments || '{}'); } catch (_) {}

        var ax = ajaxBase();
        var body = new URLSearchParams();
        body.set('action', this.toolAction);
        body.set('security', ax.nonce || '');
        body.set('nonce',    ax.nonce || '');
        body.set('tool', name);
        body.set('args', JSON.stringify(args));

        fetch(ax.ajaxurl, {
            method: 'POST', credentials: 'same-origin',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
            body: body.toString(),
        }).then(function (r) { return r.json(); }).then(function (j) {
            var output = (j && j.success && j.data && j.data.result !== undefined)
                ? j.data.result
                : { error: (j && j.data && j.data.message) || 'tool failed' };
            self._send({
                type: 'conversation.item.create',
                item: {
                    type: 'function_call_output',
                    call_id: callId,
                    output: JSON.stringify(output),
                },
            });
            self._send({ type: 'response.create' });
        }).catch(function (err) {
            self._log('tool bridge error', err);
            self._send({
                type: 'conversation.item.create',
                item: {
                    type: 'function_call_output',
                    call_id: callId,
                    output: JSON.stringify({ error: String(err && err.message || err) }),
                },
            });
            self._send({ type: 'response.create' });
        });
    };

    // --- 7. Mute / unmute ----------------------------------------------------
    VMORealtimeSession.prototype.mute = function () {
        this.muted = true;
        if (this.localStream) this.localStream.getAudioTracks().forEach(function (t) { t.enabled = false; });
    };
    VMORealtimeSession.prototype.unmute = function () {
        this.muted = false;
        if (this.localStream) this.localStream.getAudioTracks().forEach(function (t) { t.enabled = true; });
    };

    // --- 8. Stop / teardown --------------------------------------------------
    VMORealtimeSession.prototype.stop = function () {
        this._teardown();
        this._setStatus('ended');
    };
    VMORealtimeSession.prototype._teardown = function () {
        try { if (this.dc) this.dc.close(); } catch (_) {}
        try { if (this.pc) this.pc.close(); } catch (_) {}
        try {
            if (this.localStream) this.localStream.getTracks().forEach(function (t) { try { t.stop(); } catch (_) {} });
        } catch (_) {}
        try {
            if (this.audioSink) { this.audioSink.pause(); this.audioSink.srcObject = null; }
        } catch (_) {}
        this.dc = null; this.pc = null; this.localStream = null;
        this.activeResponseId = null;
    };

    // Allow other modules to find and reuse an active session (e.g. the
    // consultation flow piggy-backing on the Jarvis panel).
    VMORealtimeSession.shared = null;

    global.VMORealtimeSession = VMORealtimeSession;
})(window);
