// voice-mesh.js — self-hosted WebRTC mesh voice client (Phase C of the my-sea
// invite/voice sprint, per disco-voice-mesh.pdf). Full-mesh: every peer holds
// a direct RTCPeerConnection to every other peer; the Django Channels
// `RoomVoiceConsumer` only relays signaling (offer/answer/ice). TURN creds
// come from /api/voice/turn-credentials/.
//
// Exposes `window.VoiceRoom` { join(roomId), leave(), toggleMute() } and the
// pure `window.tuneOpus(sdp)` helper (Jasmine-tested).
(function () {
    'use strict';

    // Munge an SDP offer/answer so the Opus payload runs in-band FEC (packet-
    // loss resilience), DTX (silence suppression), and a ~40 kbps cap — voice,
    // not music. Pure string transform so it's unit-testable without WebRTC.
    function tuneOpus(sdp) {
        if (!sdp || sdp.indexOf('opus') === -1) return sdp;
        var m = sdp.match(/a=rtpmap:(\d+)\s+opus\/48000/i);
        if (!m) return sdp;
        var pt = m[1];
        var fmtpRe = new RegExp('a=fmtp:' + pt + ' ([^\\r\\n]*)');
        var extra = 'useinbandfec=1;usedtx=1;maxaveragebitrate=40000';
        if (fmtpRe.test(sdp)) {
            return sdp.replace(fmtpRe, function (line, params) {
                return 'a=fmtp:' + pt + ' ' + params + ';' + extra;
            });
        }
        // No existing fmtp line for opus — append one after its rtpmap.
        return sdp.replace(
            m[0], m[0] + '\r\na=fmtp:' + pt + ' ' + extra
        );
    }
    window.tuneOpus = tuneOpus;

    var ICE_FALLBACK = [{ urls: 'stun:stun.l.google.com:19302' }];

    function VoiceRoom() {
        this.ws = null;
        this.localStream = null;
        this.peers = {};        // peerId → RTCPeerConnection
        this.iceServers = ICE_FALLBACK;
        this.selfId = null;
        this.muted = false;
    }

    VoiceRoom.prototype._fetchTurn = function () {
        return fetch('/api/voice/turn-credentials/', {
            headers: { 'Accept': 'application/json' },
            credentials: 'same-origin',
        }).then(function (r) {
            return r.ok ? r.json() : null;
        }).catch(function () { return null; });
    };

    VoiceRoom.prototype._mkPeer = function (peerId) {
        var self = this;
        var pc = new RTCPeerConnection({ iceServers: this.iceServers });
        this.localStream.getTracks().forEach(function (t) {
            pc.addTrack(t, self.localStream);
        });
        pc.onicecandidate = function (e) {
            if (e.candidate) {
                self._send({ type: 'ice', target: peerId, payload: e.candidate });
            }
        };
        pc.ontrack = function (e) {
            var el = document.getElementById('voice-audio-' + peerId);
            if (!el) {
                el = document.createElement('audio');
                el.id = 'voice-audio-' + peerId;
                el.autoplay = true;
                document.body.appendChild(el);
            }
            el.srcObject = e.streams[0];
        };
        this.peers[peerId] = pc;
        return pc;
    };

    VoiceRoom.prototype._offerTo = function (peerId) {
        var self = this;
        var pc = this._mkPeer(peerId);
        return pc.createOffer().then(function (offer) {
            offer.sdp = tuneOpus(offer.sdp);
            return pc.setLocalDescription(offer);
        }).then(function () {
            self._send({ type: 'offer', target: peerId, payload: pc.localDescription });
        });
    };

    VoiceRoom.prototype._onOffer = function (src, payload) {
        var self = this;
        var pc = this.peers[src] || this._mkPeer(src);
        return pc.setRemoteDescription(payload).then(function () {
            return pc.createAnswer();
        }).then(function (answer) {
            answer.sdp = tuneOpus(answer.sdp);
            return pc.setLocalDescription(answer);
        }).then(function () {
            self._send({ type: 'answer', target: src, payload: pc.localDescription });
        });
    };

    VoiceRoom.prototype._dropPeer = function (peerId) {
        var pc = this.peers[peerId];
        if (pc) { try { pc.close(); } catch (e) {} delete this.peers[peerId]; }
        var el = document.getElementById('voice-audio-' + peerId);
        if (el && el.parentNode) el.parentNode.removeChild(el);
    };

    VoiceRoom.prototype._send = function (obj) {
        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify(obj));
        }
    };

    VoiceRoom.prototype._onMessage = function (msg) {
        var self = this;
        switch (msg.type) {
            case 'welcome':
                this.selfId = msg.peer_id;
                this._send({ type: 'hello' });
                break;
            case 'present':            // an existing peer → we offer to them
                this._offerTo(msg.source);
                break;
            case 'hello':              // a newcomer arrived → await their offer
                break;
            case 'offer':
                this._onOffer(msg.source, msg.payload);
                break;
            case 'answer':
                var pc = this.peers[msg.source];
                if (pc) pc.setRemoteDescription(msg.payload);
                break;
            case 'ice':
                var p = this.peers[msg.source];
                if (p && msg.payload) {
                    p.addIceCandidate(msg.payload).catch(function () {});
                }
                break;
            case 'left':
                this._dropPeer(msg.source);
                break;
        }
    };

    VoiceRoom.prototype.join = function (roomId) {
        var self = this;
        return this._fetchTurn().then(function (creds) {
            if (creds && creds.iceServers) self.iceServers = creds.iceServers;
            return navigator.mediaDevices.getUserMedia({
                audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
                video: false,
            });
        }).then(function (stream) {
            self.localStream = stream;
            var scheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
            self.ws = new WebSocket(scheme + '://' + window.location.host + '/ws/voice/' + roomId + '/');
            self.ws.onmessage = function (e) { self._onMessage(JSON.parse(e.data)); };
            self.ws.onclose = function () { self._teardown(); };
            return self;
        });
    };

    VoiceRoom.prototype.toggleMute = function () {
        this.muted = !this.muted;
        if (this.localStream) {
            this.localStream.getAudioTracks().forEach(function (t) {
                t.enabled = !this.muted;
            }, this);
        }
        return this.muted;
    };

    VoiceRoom.prototype._teardown = function () {
        Object.keys(this.peers).forEach(this._dropPeer.bind(this));
        if (this.localStream) {
            this.localStream.getTracks().forEach(function (t) { t.stop(); });
            this.localStream = null;
        }
    };

    VoiceRoom.prototype.leave = function () {
        if (this.ws) { try { this.ws.close(); } catch (e) {} this.ws = null; }
        this._teardown();
    };

    // Singleton for the page — burger-btn.js drives join/toggle/leave.
    window.VoiceRoom = window.VoiceRoom || new VoiceRoom();
}());
