import { parseTranscriptionResult, tryParseCoreError } from './liveAudioTranscriptionTypes.js';
/**
 * Audio format settings for a streaming session.
 * Must be configured before calling start().
 * Settings are frozen once the session starts.
 */
export class LiveAudioTranscriptionOptions {
    /** PCM sample rate in Hz. Default: 16000. */
    sampleRate = 16000;
    /** Number of audio channels. Default: 1 (mono). */
    channels = 1;
    /** Bits per sample. Default: 16. */
    bitsPerSample = 16;
    /** Optional BCP-47 language hint (e.g., "en", "zh"). */
    language;
    /** Maximum number of audio chunks buffered in the internal push queue. Default: 100. */
    pushQueueCapacity = 100;
    /** @internal Create a frozen copy of these settings. */
    snapshot() {
        const copy = new LiveAudioTranscriptionOptions();
        copy.sampleRate = this.sampleRate;
        copy.channels = this.channels;
        copy.bitsPerSample = this.bitsPerSample;
        copy.language = this.language;
        copy.pushQueueCapacity = this.pushQueueCapacity;
        return Object.freeze(copy);
    }
}
/**
 * Internal async queue that acts like C#'s Channel<T>.
 * Supports a single consumer reading via async iteration and multiple producers writing.
 * @internal
 */
class AsyncQueue {
    queue = [];
    waitingResolve = null;
    completed = false;
    completionError = null;
    maxCapacity;
    backpressureQueue = [];
    constructor(maxCapacity = Infinity) {
        this.maxCapacity = maxCapacity;
    }
    /** Push an item. If at capacity, waits until space is available. */
    async write(item) {
        if (this.completed) {
            throw new Error('Cannot write to a completed queue.');
        }
        if (this.waitingResolve) {
            const resolve = this.waitingResolve;
            this.waitingResolve = null;
            resolve({ value: item, done: false });
            return;
        }
        while (this.queue.length >= this.maxCapacity) {
            await new Promise((resolve) => {
                this.backpressureQueue.push(resolve);
            });
        }
        if (this.completed) {
            throw new Error('Cannot write to a completed queue.');
        }
        this.queue.push(item);
    }
    /** Push an item synchronously (no backpressure wait). Returns false if completed or at capacity. */
    tryWrite(item) {
        if (this.completed)
            return false;
        if (this.waitingResolve) {
            const resolve = this.waitingResolve;
            this.waitingResolve = null;
            resolve({ value: item, done: false });
            return true;
        }
        if (this.queue.length >= this.maxCapacity) {
            return false;
        }
        this.queue.push(item);
        return true;
    }
    /** Signal that no more items will be written. */
    complete(error) {
        if (this.completed)
            return;
        this.completed = true;
        this.completionError = error ?? null;
        // Release all blocked writers
        for (const resolve of this.backpressureQueue) {
            resolve();
        }
        this.backpressureQueue = [];
        if (this.waitingResolve) {
            const resolve = this.waitingResolve;
            this.waitingResolve = null;
            resolve({ value: undefined, done: true });
        }
    }
    get error() {
        return this.completionError;
    }
    /** Async iterator for consuming items. */
    async *[Symbol.asyncIterator]() {
        while (true) {
            if (this.backpressureQueue.length > 0 && this.queue.length < this.maxCapacity) {
                const resolve = this.backpressureQueue.shift();
                resolve();
            }
            if (this.queue.length > 0) {
                yield this.queue.shift();
                continue;
            }
            if (this.completed) {
                if (this.completionError) {
                    throw this.completionError;
                }
                return;
            }
            const result = await new Promise((resolve) => {
                this.waitingResolve = resolve;
            });
            if (result.done) {
                if (this.completionError) {
                    throw this.completionError;
                }
                return;
            }
            yield result.value;
        }
    }
}
/**
 * Client for real-time audio streaming ASR (Automatic Speech Recognition).
 * Audio data from a microphone (or other source) is pushed in as PCM chunks,
 * and transcription results are returned as an async iterable.
 *
 * Mirrors the C# LiveAudioTranscriptionSession.
 */
export class LiveAudioTranscriptionSession {
    modelId;
    coreInterop;
    sessionHandle = null;
    started = false;
    stopped = false;
    outputQueue = null;
    pushQueue = null;
    pushLoopPromise = null;
    activeSettings = null;
    sessionAbortController = null;
    streamConsumed = false;
    /**
     * Configuration settings for the streaming session.
     * Must be configured before calling start(). Settings are snapshotted at start();
     * changes made after start() are ignored for the current session.
     */
    settings = new LiveAudioTranscriptionOptions();
    /**
     * @internal
     * Users should create sessions via AudioClient.createLiveTranscriptionSession().
     */
    constructor(modelId, coreInterop) {
        this.modelId = modelId;
        this.coreInterop = coreInterop;
    }
    /**
     * Start a real-time audio streaming session.
     * Must be called before append() or getTranscriptionStream().
     * Settings are frozen after this call.
     */
    async start() {
        if (this.started) {
            throw new Error('Streaming session already started. Call stop() first.');
        }
        this.activeSettings = this.settings.snapshot();
        this.outputQueue = new AsyncQueue();
        this.pushQueue = new AsyncQueue(this.activeSettings.pushQueueCapacity);
        this.streamConsumed = false;
        const params = {
            Model: this.modelId,
            SampleRate: this.activeSettings.sampleRate.toString(),
            Channels: this.activeSettings.channels.toString(),
            BitsPerSample: this.activeSettings.bitsPerSample.toString(),
        };
        if (this.activeSettings.language) {
            params['Language'] = this.activeSettings.language;
        }
        try {
            const response = this.coreInterop.executeCommand("audio_stream_start", {
                Params: params
            });
            this.sessionHandle = response;
            if (!this.sessionHandle) {
                throw new Error('Native core did not return a session handle.');
            }
        }
        catch (error) {
            const err = new Error(`Error starting audio stream session: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
            this.outputQueue.complete(err);
            throw err;
        }
        this.started = true;
        this.stopped = false;
        this.sessionAbortController = new AbortController();
        this.pushLoopPromise = this.pushLoop();
    }
    /**
     * Push a chunk of raw PCM audio data to the streaming session.
     * Can be called from any context. Chunks are internally queued
     * and serialized to native core one at a time.
     *
     * @param pcmData - Raw PCM audio bytes matching the configured format.
     */
    async append(pcmData) {
        if (!this.started || this.stopped) {
            throw new Error('No active streaming session. Call start() first.');
        }
        const copy = new Uint8Array(pcmData.length);
        copy.set(pcmData);
        await this.pushQueue.write(copy);
    }
    /**
     * Internal loop that drains the push queue and sends chunks to native core one at a time.
     * Terminates the session on any native error.
     * @internal
     */
    async pushLoop() {
        try {
            for await (const audioData of this.pushQueue) {
                if (this.sessionAbortController?.signal.aborted) {
                    break;
                }
                try {
                    const responseData = this.coreInterop.executeCommandWithBinary("audio_stream_push", {
                        Params: {
                            SessionHandle: this.sessionHandle,
                        }
                    }, audioData);
                    // Parse transcription result from push response and surface it
                    if (responseData) {
                        try {
                            const result = parseTranscriptionResult(responseData);
                            const text = result.content?.[0]?.text;
                            if (text !== undefined && text !== null && text !== '') {
                                this.outputQueue?.tryWrite(result);
                            }
                        }
                        catch {
                            // Non-fatal: log and continue if response isn't a transcription result
                        }
                    }
                }
                catch (error) {
                    const errorMsg = error instanceof Error ? error.message : String(error);
                    const errorInfo = tryParseCoreError(errorMsg);
                    const fatalError = new Error(`Push failed (code=${errorInfo?.code ?? 'UNKNOWN'}): ${errorMsg}`, { cause: error });
                    this.stopped = true;
                    this.started = false;
                    this.pushQueue?.complete(fatalError);
                    this.outputQueue?.complete(fatalError);
                    return;
                }
            }
        }
        catch (error) {
            if (this.sessionAbortController?.signal.aborted) {
                return;
            }
            const err = error instanceof Error ? error : new Error(String(error));
            this.outputQueue?.complete(new Error('Push loop terminated unexpectedly.', { cause: err }));
        }
    }
    /**
     * Get the async iterable of transcription results.
     * Results arrive as the native ASR engine processes audio data.
     *
     * Usage:
     * ```ts
     * for await (const result of client.getTranscriptionStream()) {
     *     console.log(result.content[0].text);
     * }
     * ```
     */
    async *getTranscriptionStream() {
        if (!this.outputQueue) {
            throw new Error('No active streaming session. Call start() first.');
        }
        if (this.streamConsumed) {
            throw new Error('getTranscriptionStream() can only be called once per session. The output stream has already been consumed.');
        }
        this.streamConsumed = true;
        for await (const item of this.outputQueue) {
            yield item;
        }
    }
    /**
     * Signal end-of-audio and stop the streaming session.
     * Any remaining buffered audio in the push queue will be drained to native core first.
     * Final results are delivered through getTranscriptionStream() before it completes.
     */
    async stop() {
        if (!this.started || this.stopped) {
            return;
        }
        this.stopped = true;
        this.pushQueue?.complete();
        if (this.pushLoopPromise) {
            await this.pushLoopPromise;
        }
        this.sessionAbortController?.abort();
        let stopError = null;
        try {
            const responseData = this.coreInterop.executeCommand("audio_stream_stop", {
                Params: { SessionHandle: this.sessionHandle }
            });
            // Parse final transcription from stop response
            if (responseData) {
                try {
                    const finalResult = parseTranscriptionResult(responseData);
                    if (finalResult.content?.[0]?.text) {
                        this.outputQueue?.tryWrite(finalResult);
                    }
                }
                catch {
                    // Non-fatal
                }
            }
        }
        catch (error) {
            stopError = error instanceof Error ? error : new Error(String(error));
        }
        this.sessionHandle = null;
        this.started = false;
        this.sessionAbortController = null;
        this.outputQueue?.complete();
        if (stopError) {
            throw new Error(`Error stopping audio stream session: ${stopError.message}`, { cause: stopError });
        }
    }
    /**
     * Dispose the client and stop any active session.
     * Safe to call multiple times.
     */
    async dispose() {
        try {
            if (this.started && !this.stopped) {
                await this.stop();
            }
        }
        catch {
            // Swallow errors during best-effort cleanup to keep dispose() silent.
        }
    }
}
