/**
 * Agent Voice Orb — Three.js 3D Blob Sphere
 *
 * Glasartige 3D-Kugel mit Noise-Displacement, Rotation und Audio-Reaktivität.
 * Three.js Objekte werden AUSSERHALB von Alpine gespeichert um Proxy-Konflikte zu vermeiden.
 */
import { WebGLRenderer, Scene, PerspectiveCamera, IcosahedronGeometry, ShaderMaterial, Mesh, Color, DoubleSide } from 'three';

// ── Three.js State (NICHT reaktiv, kein Alpine Proxy) ────────────────
const _threeState = new WeakMap();

function getThree(el) { return _threeState.get(el) || null; }
function setThree(el, state) { _threeState.set(el, state); }
function clearThree(el) { _threeState.delete(el); }

// ── Vertex Shader ────────────────────────────────────────────────────
const vertexShader = `
    uniform float uTime;
    uniform float uDisplacement;
    uniform float uSpeed;
    varying vec3 vNormal;
    varying vec3 vViewPos;
    varying float vDisp;

    vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}
    vec4 mod289(vec4 x){return x-floor(x*(1.0/289.0))*289.0;}
    vec4 permute(vec4 x){return mod289((x*34.0+1.0)*x);}
    vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-0.85373472095314*r;}

    float snoise(vec3 v){
        const vec2 C=vec2(1.0/6.0,1.0/3.0);
        const vec4 D=vec4(0.0,0.5,1.0,2.0);
        vec3 i=floor(v+dot(v,C.yyy));
        vec3 x0=v-i+dot(i,C.xxx);
        vec3 g=step(x0.yzx,x0.xyz);
        vec3 l=1.0-g;
        vec3 i1=min(g.xyz,l.zxy);
        vec3 i2=max(g.xyz,l.zxy);
        vec3 x1=x0-i1+C.xxx;
        vec3 x2=x0-i2+C.yyy;
        vec3 x3=x0-D.yyy;
        i=mod289(i);
        vec4 p=permute(permute(permute(
            i.z+vec4(0.0,i1.z,i2.z,1.0))
            +i.y+vec4(0.0,i1.y,i2.y,1.0))
            +i.x+vec4(0.0,i1.x,i2.x,1.0));
        float n_=0.142857142857;
        vec3 ns=n_*D.wyz-D.xzx;
        vec4 j=p-49.0*floor(p*ns.z*ns.z);
        vec4 x_=floor(j*ns.z);
        vec4 y_=floor(j-7.0*x_);
        vec4 x=x_*ns.x+ns.yyyy;
        vec4 y=y_*ns.x+ns.yyyy;
        vec4 h=1.0-abs(x)-abs(y);
        vec4 b0=vec4(x.xy,y.xy);
        vec4 b1=vec4(x.zw,y.zw);
        vec4 s0=floor(b0)*2.0+1.0;
        vec4 s1=floor(b1)*2.0+1.0;
        vec4 sh=-step(h,vec4(0.0));
        vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy;
        vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;
        vec3 p0=vec3(a0.xy,h.x);
        vec3 p1=vec3(a0.zw,h.y);
        vec3 p2=vec3(a1.xy,h.z);
        vec3 p3=vec3(a1.zw,h.w);
        vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));
        p0*=norm.x;p1*=norm.y;p2*=norm.z;p3*=norm.w;
        vec4 m=max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0);
        m=m*m;
        return 42.0*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));
    }

    void main(){
        float t = uTime * uSpeed;
        vec3 pos = position;

        float n1 = snoise(pos * 1.5 + t * 0.3) * uDisplacement;
        float n2 = snoise(pos * 3.0 - t * 0.2) * uDisplacement * 0.35;
        float n3 = snoise(pos * 5.0 + t * 0.5) * uDisplacement * 0.15;
        float disp = n1 + n2 + n3;

        vDisp = disp;
        pos += normal * disp;
        vNormal = normalize(normalMatrix * normal);
        vViewPos = (modelViewMatrix * vec4(pos, 1.0)).xyz;

        gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
    }
`;

// ── Fragment Shader ──────────────────────────────────────────────────
const fragmentShader = `
    uniform float uTime;
    uniform vec3 uColor1;
    uniform vec3 uColor2;
    uniform float uOpacity;
    uniform float uFresnelPower;
    varying vec3 vNormal;
    varying vec3 vViewPos;
    varying float vDisp;

    void main(){
        vec3 viewDir = normalize(-vViewPos);
        float fresnel = pow(1.0 - max(dot(viewDir, vNormal), 0.0), uFresnelPower);

        float colorMix = smoothstep(-0.15, 0.15, vDisp);
        vec3 color = mix(uColor1, uColor2, colorMix);

        color += fresnel * vec3(0.35, 0.35, 0.55);

        float shimmer = sin(vDisp * 30.0 + uTime * 2.0) * 0.03;
        color += shimmer;

        float alpha = mix(uOpacity, min(1.0, uOpacity + 0.3), fresnel);
        gl_FragColor = vec4(color, alpha);
    }
`;


export default function agentVoiceOrb() {
    return {
        // State (Alpine-reaktiv, OK)
        voiceMode: false,
        voiceState: 'idle',
        transcript: '',
        supported: false,

        // Audio (primitives, OK als Alpine props)
        _energy: 0,
        _targetEnergy: 0,
        _currentDisp: 0.04,
        _targetDisp: 0.04,
        _currentSpeed: 0.4,
        _targetSpeed: 0.4,

        // Audio nodes (nicht reaktiv nötig, aber simpel genug)
        _audioCtx: null,
        _micAnalyser: null,
        _playbackAnalyser: null,
        _analyser: null,
        _micStream: null,
        _micSource: null,
        _recognition: null,
        _dataArray: null,
        _silenceTimeout: null,
        _lastSpeechTime: 0,

        initVoice() {
            this.supported = 'webkitSpeechRecognition' in window || 'SpeechRecognition' in window;
        },

        toggleVoice() {
            this.voiceMode ? this.stopVoice() : this.startVoice();
        },

        async startVoice() {
            this.voiceMode = true;
            this.voiceState = 'idle';
            await this.$nextTick();

            this._initThree();
            this._initAudio();

            setTimeout(() => this.startListening(), 500);
        },

        stopVoice() {
            this.voiceMode = false;
            this.voiceState = 'idle';
            this._stopMic();
            this._stopRecognition();
            this._destroyThree();
            if (this._audioCtx) {
                this._audioCtx.close().catch(() => {});
                this._audioCtx = null;
                this._micAnalyser = null;
                this._playbackAnalyser = null;
                this._analyser = null;
            }
        },

        // ── Three.js (alles in externem State, kein Proxy) ──────
        _initThree() {
            const container = this.$refs.orbContainer;
            if (!container) return;

            const size = 280;
            const scene = new Scene();
            const camera = new PerspectiveCamera(45, 1, 0.1, 100);
            camera.position.z = 3.2;

            const renderer = new WebGLRenderer({ alpha: true, antialias: true });
            renderer.setSize(size, size);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
            renderer.setClearColor(0x000000, 0);
            container.innerHTML = '';
            container.appendChild(renderer.domElement);

            const geo = new IcosahedronGeometry(1, 64);

            const material = new ShaderMaterial({
                vertexShader,
                fragmentShader,
                uniforms: {
                    uTime: { value: 0 },
                    uDisplacement: { value: 0.05 },
                    uSpeed: { value: 0.4 },
                    uColor1: { value: new Color(0.35, 0.45, 0.95) },
                    uColor2: { value: new Color(0.55, 0.35, 0.95) },
                    uOpacity: { value: 0.85 },
                    uFresnelPower: { value: 2.5 },
                },
                transparent: true,
                side: DoubleSide,
            });

            const mesh = new Mesh(geo, material);
            scene.add(mesh);

            // In WeakMap speichern, nicht in Alpine
            setThree(container, {
                scene, camera, renderer, material, mesh, geo,
                startTime: performance.now(),
                color1: new Color(0.35, 0.45, 0.95),
                color2: new Color(0.55, 0.35, 0.95),
                targetColor1: new Color(0.35, 0.45, 0.95),
                targetColor2: new Color(0.55, 0.35, 0.95),
            });

            this._animateThree(container);
        },

        _destroyThree() {
            const container = this.$refs.orbContainer;
            if (!container) return;

            const s = getThree(container);
            if (!s) return;

            s.renderer.dispose();
            s.material.dispose();
            s.geo.dispose();
            s.renderer.domElement.remove();
            clearThree(container);
        },

        _animateThree(container) {
            const s = getThree(container);
            if (!s || !this.voiceMode) return;

            requestAnimationFrame(() => this._animateThree(container));

            const now = performance.now();
            const elapsed = (now - s.startTime) / 1000;
            const dt = Math.min(1 / 30, 1 / 60); // cap

            // Audio energy
            if (this._analyser && this._dataArray) {
                this._analyser.getByteFrequencyData(this._dataArray);
                let sum = 0;
                for (let i = 0; i < this._dataArray.length; i++) sum += this._dataArray[i];
                this._targetEnergy = sum / this._dataArray.length / 255;
            } else {
                this._targetEnergy = 0;
            }
            this._energy += (this._targetEnergy - this._energy) * 0.12;

            // State targets
            switch (this.voiceState) {
                case 'listening':
                    this._targetDisp = 0.06 + this._energy * 0.25;
                    this._targetSpeed = 0.6 + this._energy * 0.4;
                    s.targetColor1 = new Color(0.3, 0.5, 1.0);
                    s.targetColor2 = new Color(0.4, 0.7, 1.0);
                    break;
                case 'thinking':
                    this._targetDisp = 0.08 + Math.sin(elapsed * 3) * 0.03;
                    this._targetSpeed = 1.2;
                    s.targetColor1 = new Color(0.5, 0.3, 0.9);
                    s.targetColor2 = new Color(0.7, 0.3, 1.0);
                    break;
                case 'speaking':
                    this._targetDisp = 0.08 + this._energy * 0.35;
                    this._targetSpeed = 0.8 + this._energy * 0.5;
                    s.targetColor1 = new Color(0.35, 0.4, 1.0);
                    s.targetColor2 = new Color(0.5, 0.55, 1.0);
                    break;
                default:
                    this._targetDisp = 0.04 + Math.sin(elapsed) * 0.015;
                    this._targetSpeed = 0.3;
                    s.targetColor1 = new Color(0.35, 0.45, 0.95);
                    s.targetColor2 = new Color(0.55, 0.35, 0.95);
            }

            // Smooth lerp
            const lerpRate = 0.06;
            this._currentDisp += (this._targetDisp - this._currentDisp) * 0.1;
            this._currentSpeed += (this._targetSpeed - this._currentSpeed) * lerpRate;
            s.color1.lerp(s.targetColor1, lerpRate);
            s.color2.lerp(s.targetColor2, lerpRate);

            // Update uniforms
            const u = s.material.uniforms;
            u.uTime.value = elapsed;
            u.uDisplacement.value = this._currentDisp;
            u.uSpeed.value = this._currentSpeed;
            u.uColor1.value.copy(s.color1);
            u.uColor2.value.copy(s.color2);

            // Rotation
            s.mesh.rotation.y = elapsed * 0.15;
            s.mesh.rotation.x = Math.sin(elapsed * 0.1) * 0.1;

            s.renderer.render(s.scene, s.camera);
        },

        // ── Audio ─────────────────────────────────────────
        _initAudio() {
            this._audioCtx = new (window.AudioContext || window.webkitAudioContext)();

            this._micAnalyser = this._audioCtx.createAnalyser();
            this._micAnalyser.fftSize = 256;
            this._micAnalyser.smoothingTimeConstant = 0.8;

            this._playbackAnalyser = this._audioCtx.createAnalyser();
            this._playbackAnalyser.fftSize = 256;
            this._playbackAnalyser.smoothingTimeConstant = 0.8;
            this._playbackAnalyser.connect(this._audioCtx.destination);

            this._analyser = this._micAnalyser;
            this._dataArray = new Uint8Array(128);
        },

        // ── Speech Recognition ────────────────────────────
        startListening() {
            if (!this.voiceMode) return;

            this.voiceState = 'listening';
            this.transcript = '';
            this._lastSpeechTime = Date.now();
            this._analyser = this._micAnalyser;

            navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
                this._micStream = stream;
                if (this._audioCtx && this._micAnalyser) {
                    if (this._micSource) { try { this._micSource.disconnect(); } catch(e) {} }
                    this._micSource = this._audioCtx.createMediaStreamSource(stream);
                    this._micSource.connect(this._micAnalyser);
                }
            }).catch(() => {});

            const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
            if (!SR) return;

            this._recognition = new SR();
            this._recognition.lang = 'de-DE';
            this._recognition.interimResults = true;
            this._recognition.continuous = true;
            this._recognition.maxAlternatives = 1;

            let finalTranscript = '';

            this._recognition.onresult = (event) => {
                this._lastSpeechTime = Date.now();
                let interim = '';
                for (let i = event.resultIndex; i < event.results.length; i++) {
                    if (event.results[i].isFinal) {
                        finalTranscript += event.results[i][0].transcript + ' ';
                    } else {
                        interim += event.results[i][0].transcript;
                    }
                }
                this.transcript = (finalTranscript + interim).trim();

                clearTimeout(this._silenceTimeout);
                this._silenceTimeout = setTimeout(() => {
                    if (this.voiceState !== 'listening') return;
                    if (this.transcript.trim()) {
                        const text = this.transcript.trim();
                        this._stopRecognition();
                        this._stopMic();
                        this._sendVoiceMessage(text);
                    }
                }, 2000);
            };

            this._recognition.onend = () => {
                if (this.voiceMode && this.voiceState === 'listening') {
                    if (this.transcript.trim() && Date.now() - this._lastSpeechTime > 1500) {
                        this._sendVoiceMessage(this.transcript.trim());
                    } else {
                        setTimeout(() => {
                            if (this.voiceMode && this.voiceState === 'listening') {
                                try { this._recognition.start(); } catch(e) {}
                            }
                        }, 100);
                    }
                }
            };

            this._recognition.onerror = (event) => {
                if (['no-speech', 'aborted'].includes(event.error) && this.voiceMode && this.voiceState === 'listening') {
                    setTimeout(() => {
                        if (this.voiceMode && this.voiceState === 'listening') {
                            try { this._recognition.start(); } catch(e) {}
                        }
                    }, 200);
                }
            };

            try { this._recognition.start(); } catch(e) {}
        },

        async _sendVoiceMessage(text) {
            this.voiceState = 'thinking';
            clearTimeout(this._silenceTimeout);
            this._stopMic();
            this._stopRecognition();

            this.pendingMessage = text;

            try {
                // send() mit withAudio=true → liefert Audio im selben Request zurück
                const result = await this.$wire.call('send', text, true);
                this.pendingMessage = null;
                this.sending = false;

                const conv = this.$wire.conversation;
                const last = conv?.length > 0 ? conv[conv.length - 1] : null;

                if (last?.role === 'assistant') {
                    const msg = last.content;
                    const shouldEnd = msg.includes('[END]');
                    const cleanMsg = msg.replace('[END]', '').trim();

                    if (cleanMsg) {
                        // Audio aus dem kombinierten Response nutzen (kein zweiter Call)
                        if (result?.audio) {
                            this.voiceState = 'speaking';
                            this._analyser = this._playbackAnalyser;
                            await this._playAudio(result.audio);
                        } else {
                            await this._speak(cleanMsg);
                        }
                    }

                    if (shouldEnd) {
                        this.stopVoice();
                        return;
                    }
                }
            } catch (e) {
                this.pendingMessage = null;
                this.sending = false;
            }

            // Weiter zuhören
            if (this.voiceMode) {
                this.voiceState = 'idle';
                setTimeout(() => {
                    if (this.voiceMode) this.startListening();
                }, 1000);
            }
        },

        async _speak(text) {
            this.voiceState = 'speaking';
            this._analyser = this._playbackAnalyser;

            try {
                // Race: TTS-API vs. 15s Timeout
                const result = await Promise.race([
                    this.$wire.call('synthesize', text),
                    new Promise(resolve => setTimeout(() => resolve(null), 15000)),
                ]);

                if (result?.audio) {
                    await this._playAudio(result.audio);
                } else {
                    // API hat nichts zurückgegeben oder Timeout → Browser-TTS
                    await this._browserTTS(text);
                }
            } catch (e) {
                await this._browserTTS(text);
            }
        },

        _browserTTS(text) {
            return new Promise(resolve => {
                const u = new SpeechSynthesisUtterance(text);
                u.lang = 'de-DE';
                u.onend = resolve;
                u.onerror = resolve;
                speechSynthesis.speak(u);
            });
        },

        _playAudio(base64) {
            return new Promise(resolve => {
                const audio = new Audio('data:audio/mp3;base64,' + base64);
                if (this._audioCtx && this._playbackAnalyser) {
                    try {
                        const src = this._audioCtx.createMediaElementSource(audio);
                        src.connect(this._playbackAnalyser);
                    } catch(e) {}
                }
                audio.onended = resolve;
                audio.onerror = resolve;
                audio.play().catch(resolve);
            });
        },

        _stopMic() {
            if (this._micSource) { try { this._micSource.disconnect(); } catch(e) {} this._micSource = null; }
            if (this._micStream) { this._micStream.getTracks().forEach(t => t.stop()); this._micStream = null; }
        },

        _stopRecognition() {
            clearTimeout(this._silenceTimeout);
            if (this._recognition) { try { this._recognition.abort(); } catch(e) {} this._recognition = null; }
        },
    };
}
