/** @odoo-module **/
// Fusion Notes - Voice Recording Service
// Copyright 2026 Nexa Systems Inc.
// License OPL-1
//
// Provides AudioRecorder (MediaRecorder API) and SpeechRecognizer
// (Web Speech API fallback) for voice-to-text in Odoo chatter.

/**
 * AudioRecorder - Records microphone audio using the MediaRecorder API.
 *
 * Usage:
 *   const recorder = new AudioRecorder();
 *   await recorder.start();         // request mic + begin recording
 *   const blob = await recorder.stop();  // stop and get audio Blob
 *   recorder.cancel();              // abort without producing output
 */
export class AudioRecorder {
    constructor() {
        this.mediaRecorder = null;
        this.audioChunks = [];
        this.stream = null;
        this.mimeType = this._getSupportedMimeType();
    }

    /**
     * Pick the best supported MIME type for recording.
     * Whisper accepts webm, ogg, mp4, mp3, wav, etc.
     */
    _getSupportedMimeType() {
        const types = [
            'audio/webm;codecs=opus',
            'audio/webm',
            'audio/ogg;codecs=opus',
            'audio/ogg',
            'audio/mp4',
        ];
        for (const type of types) {
            if (
                typeof MediaRecorder !== 'undefined' &&
                MediaRecorder.isTypeSupported(type)
            ) {
                return type;
            }
        }
        return 'audio/webm';
    }

    /** Base MIME type without codec suffix (for Whisper file upload). */
    get baseMimeType() {
        return this.mimeType.split(';')[0];
    }

    /** Whether MediaRecorder is available in this browser. */
    get isSupported() {
        return (
            typeof navigator !== 'undefined' &&
            navigator.mediaDevices &&
            typeof navigator.mediaDevices.getUserMedia === 'function' &&
            typeof MediaRecorder !== 'undefined'
        );
    }

    /** Request microphone access and begin recording. */
    async start() {
        if (!this.isSupported) {
            throw new Error(
                'Audio recording is not supported in this browser.'
            );
        }

        this.audioChunks = [];
        this.stream = await navigator.mediaDevices.getUserMedia({
            audio: {
                echoCancellation: true,
                noiseSuppression: true,
                sampleRate: 16000,
            },
        });

        this.mediaRecorder = new MediaRecorder(this.stream, {
            mimeType: this.mimeType,
        });

        this.mediaRecorder.ondataavailable = (event) => {
            if (event.data.size > 0) {
                this.audioChunks.push(event.data);
            }
        };

        // Collect data every second for more granular chunks
        this.mediaRecorder.start(1000);
    }

    /** Stop recording and return the audio Blob. */
    async stop() {
        return new Promise((resolve, reject) => {
            if (
                !this.mediaRecorder ||
                this.mediaRecorder.state === 'inactive'
            ) {
                reject(new Error('No active recording.'));
                return;
            }

            this.mediaRecorder.onstop = () => {
                const blob = new Blob(this.audioChunks, {
                    type: this.baseMimeType,
                });
                this._cleanup();
                resolve(blob);
            };

            this.mediaRecorder.onerror = (e) => {
                this._cleanup();
                reject(e);
            };

            this.mediaRecorder.stop();
        });
    }

    /** Cancel recording and release resources without producing output. */
    cancel() {
        if (
            this.mediaRecorder &&
            this.mediaRecorder.state !== 'inactive'
        ) {
            try {
                this.mediaRecorder.stop();
            } catch {
                // Already stopped
            }
        }
        this._cleanup();
    }

    /** Release microphone stream and reset state. */
    _cleanup() {
        if (this.stream) {
            this.stream.getTracks().forEach((track) => track.stop());
            this.stream = null;
        }
        this.mediaRecorder = null;
        this.audioChunks = [];
    }
}

/**
 * SpeechRecognizer - Fallback using the browser Web Speech API.
 *
 * Provides real-time transcription without a server round-trip.
 * Supported in Chrome, Edge, and some Chromium browsers.
 *
 * Usage:
 *   const recognizer = new SpeechRecognizer();
 *   const promise = recognizer.start();  // returns Promise<string>
 *   recognizer.stop();                   // stop and resolve promise
 *   const text = await promise;
 */
export class SpeechRecognizer {
    constructor() {
        const SR =
            window.SpeechRecognition || window.webkitSpeechRecognition;
        this.isSupported = !!SR;
        this.recognition = this.isSupported ? new SR() : null;
        this.transcript = '';
        this.isListening = false;
        this._resolveStop = null;

        if (this.recognition) {
            this.recognition.continuous = true;
            this.recognition.interimResults = true;
            this.recognition.lang = 'en-US';
        }
    }

    /**
     * Start listening. Returns a promise that resolves with the final
     * transcript when stop() is called or recognition ends naturally.
     */
    start() {
        if (!this.isSupported || !this.recognition) {
            throw new Error(
                'Speech Recognition is not supported in this browser.'
            );
        }

        this.transcript = '';
        this.isListening = true;
        let finalTranscript = '';

        return new Promise((resolve, reject) => {
            this.recognition.onresult = (event) => {
                let interimTranscript = '';
                for (let i = event.resultIndex; i < event.results.length; i++) {
                    if (event.results[i].isFinal) {
                        finalTranscript += event.results[i][0].transcript + ' ';
                    } else {
                        interimTranscript += event.results[i][0].transcript;
                    }
                }
                this.transcript = (finalTranscript + interimTranscript).trim();
            };

            this.recognition.onerror = (event) => {
                this.isListening = false;
                if (event.error === 'no-speech') {
                    resolve(finalTranscript.trim());
                } else {
                    reject(
                        new Error(`Speech recognition error: ${event.error}`)
                    );
                }
            };

            this.recognition.onend = () => {
                this.isListening = false;
                resolve(finalTranscript.trim());
            };

            this.recognition.start();
            this._resolveStop = () => {
                this.recognition.stop();
            };
        });
    }

    /** Signal recognition to stop; the start() promise will resolve. */
    stop() {
        if (this._resolveStop) {
            this._resolveStop();
        }
    }

    /** Abort immediately without waiting for results. */
    cancel() {
        if (this.recognition && this.isListening) {
            try {
                this.recognition.abort();
            } catch {
                // Already stopped
            }
        }
        this.isListening = false;
        this.transcript = '';
        this._resolveStop = null;
    }
}

/**
 * Convert a Blob to a base64-encoded string (without data-URI prefix).
 */
export function blobToBase64(blob) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onloadend = () => {
            // reader.result is "data:<mime>;base64,<data>" - extract <data>
            const base64 = reader.result.split(',')[1];
            resolve(base64);
        };
        reader.onerror = reject;
        reader.readAsDataURL(blob);
    });
}
