/**
 * SentenceQueueModule
 * Manages the preparation pipeline for sentences, including TTS generation
 */
import { BaseModule } from './base-module.js';

const TTS_GENERATION_TIMEOUT_MS = 60000;

class SentenceQueueModule extends BaseModule {
    constructor() {
        super('sentence-queue', 'Sentence Queue');
        
        // Dependencies
        this.dependencies = ['text-buffer', 'tts-factory', 'paragraph-layout', 'audio-manager', 'persistence-manager'];

        // Queue state
        this.sentenceQueue = [];
        this.isProcessing = false;
        this.onSentenceReadyCallback = null;

        // Cache in-flight TTS prefetches only. Layout belongs to the renderer.
        this.prefetchingSpeech = new Map();
        this.autoplay = true;
        this.inputMode = 'text';
        this.lastContinueAt = 0;
        this.pauseBeforeNextReason = null;
        this.ttsGenerationTimeoutMs = TTS_GENERATION_TIMEOUT_MS;
        this.generationRequests = new Map();

        // Bind methods
        this.bindMethods([
            'initialize',
            'addSentence',
            'processNextSentence',
            'setOnSentenceReady',
            'pauseBeforeNext',
            'completeSentence',
            'getCacheKey',
            'getPreparedSentence',
            'prefetchAhead',
            'prepareSpeechMetadata',
            'normalizeTtsText',
            'runTtsPreloadWithTimeout',
            'cancelBlockingGeneration',
            'cancelGenerationRequests',
            'isSpeechItem',
            'getMediaPauseSeconds',
            'readFirstFiniteNumber',
            'waitForSkippableMediaPause',
            'shouldAutoplay',
            'waitForManualContinue',
            'prepareSentence',
            'prepareLayout',
            'extractWords',
            'getDropCapText',
            'extractDropCapText',
            'calculateAnimationTiming',
            'clear'
        ]);
    }
    
    /**
     * Initialize the module
     * @returns {Promise<boolean>} - Resolves with success status
     */
    async initialize() {
        try {
            // Get dependencies
            const textBuffer = this.getModule('text-buffer');
            
            if (!textBuffer) {
                console.error("SentenceQueue: TextBuffer dependency not found");
                return false;
            }
            
            // Set up the text buffer to send sentences to this queue
            textBuffer.setOnSentenceReady((sentence, callback) => {
                this.addSentence(sentence, callback);
            });
            
            this.reportProgress(100, "Sentence queue ready");
            const persistenceManager = this.getModule('persistence-manager');
            if (persistenceManager && typeof persistenceManager.getPreference === 'function') {
                this.autoplay = persistenceManager.getPreference('app', 'autoplay', true) !== false;
            }
            this.addEventListener(document, 'preference-updated', (event) => {
                const { category, key, value } = event.detail || {};
                if (category === 'app' && key === 'autoplay') {
                    this.autoplay = value !== false;
                }
            });
            this.addEventListener(document, 'story:input-mode', (event) => {
                this.inputMode = ['text', 'choice', 'end'].includes(event.detail) ? event.detail : 'text';
            });
            this.addEventListener(document, 'ui:command', (event) => {
                if (event.detail?.type === 'continue') {
                    this.lastContinueAt = performance.now();
                    this.cancelBlockingGeneration('user-fast-forward');
                }
            });
            return true;
        } catch (error) {
            console.error("Error initializing Sentence Queue:", error);
            return false;
        }
    }
    
    /**
     * Set callback for when a sentence is ready for display
     * @param {Function} callback - Function to call with prepared sentence
     */
    setOnSentenceReady(callback) {
        if (typeof callback === 'function') {
            this.onSentenceReadyCallback = callback;
        }
    }

    pauseBeforeNext(reason = 'manual-pause') {
        this.pauseBeforeNextReason = reason;
    }
    
    /**
     * Add a sentence to the queue
     * @param {string} sentence - Sentence to add
     * @param {Function} callback - Callback to call when sentence is processed
     */
    addSentence(sentence, callback) {
        const queueItem = typeof sentence === 'object' && sentence !== null
            ? { ...sentence, callback }
            : { text: sentence, callback };

        this.sentenceQueue.push({
            ...queueItem,
            text: String(queueItem.text || '').trim()
        });
        
        // Process the queue if not already processing
        if (!this.isProcessing) {
            this.processNextSentence();
        }
    }
    
    /**
     * Process the next sentence in the queue
     */
    async processNextSentence() {
        if (this.sentenceQueue.length === 0 || this.isProcessing) {
            return;
        }

        this.isProcessing = true;
        const item = this.sentenceQueue[0];

        try {
            if (this.pauseBeforeNextReason) {
                const reason = this.pauseBeforeNextReason;
                this.pauseBeforeNextReason = null;
                await this.waitForManualContinue(reason);
            }

            const sentence = await this.getPreparedSentence(item);

            // Prefetch far enough ahead that media pauses do not block TTS
            // generation for the next spoken paragraph.
            this.prefetchAhead();

            // Notify display handler with complete sentence
            if (this.onSentenceReadyCallback) {
                await new Promise(resolve => {
                    sentence.onComplete = resolve;
                    sentence.playbackStartedAt = performance.now();
                    this.onSentenceReadyCallback(sentence, resolve);
                });
            }

            const mediaPauseSeconds = this.getMediaPauseSeconds(sentence);
            if (mediaPauseSeconds > 0) {
                await this.waitForSkippableMediaPause(mediaPauseSeconds, sentence.kind, sentence.id);
            }

            if (this.shouldPauseAfterSentence(sentence)) {
                await this.waitForManualContinue(sentence.id);
            }

            // Remove from queue and continue
            this.sentenceQueue.shift();
            if (item.callback) item.callback({ success: true });

        } catch (error) {
            console.error("SentenceQueue: Error processing sentence:", error);
            if (item.callback) item.callback({ success: false, error });
        } finally {
            this.isProcessing = false;
            if (this.sentenceQueue.length > 0) {
                this.processNextSentence();
            } else {
                document.dispatchEvent(new CustomEvent('story:process-state', {
                    detail: { state: 'ready', reason: 'queue-empty' }
                }));
                document.dispatchEvent(new CustomEvent('tts:queue-empty', {
                    detail: { reason: 'sentence-queue-empty' }
                }));
                console.log('Process state: ready', { reason: 'queue-empty' });
            }
        }
    }
    
    /**
     * Prepare speech metadata for a sentence
     * @param {string} text - Text to prepare speech for
     * @returns {Promise<Object>} - Speech metadata object
     */
    async prepareSpeechMetadata(text, context = {}) {
        const ttsFactory = this.getModule('tts-factory');

        if (!ttsFactory) {
            throw new Error("TTS dependencies not found");
        }

        const ttsText = this.normalizeTtsText(text);
        if (!ttsText) {
            console.warn('SentenceQueue: Empty TTS text after normalization, using estimated silent timing', {
                sentenceId: context.sentenceId || null
            });
            return this.estimateSpeechDuration(text);
        }

        // Check if TTS is enabled via active handler
        const activeHandler = ttsFactory.getActiveHandler();
        const isTtsEnabled = activeHandler !== null;
        
        // If TTS is disabled, estimate duration based on character count
        if (!isTtsEnabled) {
            return this.estimateSpeechDuration(text);
        }
        
        try {
            // Preload the speech to get metadata
            const result = await this.runTtsPreloadWithTimeout(ttsFactory, ttsText, context);
            
            if (!result.success) {
                console.warn("SentenceQueue: Speech preload failed, using estimated duration", {
                    reason: result.reason || 'unknown',
                    sentenceId: context.sentenceId || null,
                    textPreview: ttsText.slice(0, 80)
                });
                return this.estimateSpeechDuration(text);
            }
            
            // Create a speech metadata object
            return {
                text: ttsText,
                duration: result.duration || this.estimateSpeechDuration(text).duration,
                handler: ttsFactory.getActiveHandler() ? ttsFactory.getActiveHandler().id : null,
                audioData: result.audioData || null,
                play: async () => {
                    if (result.audioData && typeof ttsFactory.speakPreloaded === 'function') {
                        return ttsFactory.speakPreloaded(result);
                    }
                    return ttsFactory.speak(ttsText);
                },
                stop: () => {
                    return ttsFactory.stop();
                },
                isTtsEnabled: isTtsEnabled
            };
        } catch (error) {
            console.error("Error preparing speech metadata:", error);
            return this.estimateSpeechDuration(text);
        }
    }

    normalizeTtsText(text) {
        return String(text || '')
            .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, ' ')
            .replace(/\s+/g, ' ')
            .trim();
    }

    runTtsPreloadWithTimeout(ttsFactory, text, context = {}) {
        const sentenceId = context.sentenceId || context.id || `tts-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
        const requestId = `${sentenceId}:${context.prefetch ? 'prefetch' : 'blocking'}:${Date.now()}`;
        const controller = new AbortController();
        const startedAt = performance.now();

        return new Promise((resolve) => {
            let settled = false;
            const finish = (result) => {
                if (settled) return;
                settled = true;
                clearTimeout(timeoutId);
                this.generationRequests.delete(requestId);
                resolve(result);
            };

            const timeoutId = setTimeout(() => {
                console.warn('SentenceQueue: TTS generation timed out; continuing without audio', {
                    sentenceId,
                    timeoutMs: this.ttsGenerationTimeoutMs,
                    textPreview: text.slice(0, 120)
                });
                controller.abort('tts-generation-timeout');
                finish({ success: false, reason: 'tts_generation_timeout', timedOut: true });
            }, this.ttsGenerationTimeoutMs);

            this.generationRequests.set(requestId, {
                controller,
                sentenceId,
                blocking: context.blocking !== false,
                startedAt,
                textPreview: text.slice(0, 120),
                finish
            });

            Promise.resolve(ttsFactory.preloadSpeech(text, { signal: controller.signal }))
                .then(result => finish(result || { success: false, reason: 'empty_tts_result' }))
                .catch(error => {
                    if (controller.signal.aborted) {
                        console.warn('SentenceQueue: TTS generation cancelled; continuing without audio', {
                            sentenceId,
                            reason: controller.signal.reason || 'aborted',
                            elapsedMs: Math.round(performance.now() - startedAt)
                        });
                        finish({ success: false, reason: 'tts_generation_aborted', error });
                    } else {
                        console.warn('SentenceQueue: TTS generation failed; continuing without audio', {
                            sentenceId,
                            error
                        });
                        finish({ success: false, reason: 'tts_generation_error', error });
                    }
                });
        });
    }

    cancelBlockingGeneration(reason = 'cancelled') {
        this.cancelGenerationRequests(reason, request => request.blocking === true);
    }

    cancelGenerationRequests(reason = 'cancelled', predicate = () => true) {
        for (const [requestId, request] of this.generationRequests.entries()) {
            if (!predicate(request)) continue;
            console.warn('SentenceQueue: Cancelling TTS generation request', {
                requestId,
                sentenceId: request.sentenceId,
                reason,
                elapsedMs: Math.round(performance.now() - request.startedAt),
                textPreview: request.textPreview
            });
            try {
                request.controller.abort(reason);
            } catch (error) {
                console.warn('SentenceQueue: Failed to abort TTS generation request', { requestId, error });
            }
            if (typeof request.finish === 'function') {
                request.finish({ success: false, reason: 'tts_generation_cancelled' });
            }
        }
    }
    
    /**
     * Estimate speech duration based on character count
     * @param {string} text - Text to estimate duration for
     * @returns {Object} - Speech metadata object with estimated duration
     */
    estimateSpeechDuration(text) {
        // Average aloud narration is around 12 characters per second at 1x.
        const charactersPerSecond = 12;

        let speedMultiplier = 1.0;
        const ttsFactory = this.getModule('tts-factory');
        if (ttsFactory) {
            speedMultiplier = Number.isFinite(ttsFactory.speed) ? Math.max(0.25, ttsFactory.speed) : 1.0;
        }
        
        // Calculate estimated duration in milliseconds
        const charCount = text.length;
        const durationSeconds = charCount / (charactersPerSecond * speedMultiplier);
        const durationMs = Math.max(durationSeconds * 1000, 800);
        
        return {
            text: text,
            duration: durationMs,
            handler: null,
            play: async () => ({ success: false, reason: 'tts_disabled' }),
            stop: () => true,
            isTtsEnabled: false,
            isEstimated: true
        };
    }
    
    /**
     * Prepare queue metadata. This module intentionally does not create layout:
     * live rendering and history rendering must go through the same renderer.
     */
    async prepareSentence(item) {
        const text = typeof item === 'string' ? item : item.text;
        const id = item.id || `paragraph-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
        const metadata = typeof item === 'object' && item !== null ? item : {};

        try {
            if (metadata.type && !['paragraph', 'heading'].includes(metadata.type)) {
                if (metadata.type === 'music') {
                    const audioManager = this.getModule('audio-manager');
                    if (audioManager && typeof audioManager.playMusic === 'function') {
                        audioManager.getAssetUrl('music', metadata.filename);
                    }
                }

                return {
                    id,
                    kind: metadata.type,
                    text: text || '',
                    turnId: metadata.turnId ?? null,
                    blockId: metadata.blockId ?? null,
                    gameId: metadata.gameId ?? null,
                    status: 'ready',
                    metadata,
                    tts: { duration: 0, provider: null, audioData: null, play: null, stop: null, enabled: false },
                    animation: { wordTimings: [], cueTimings: [], totalDuration: 0 },
                    element: null,
                    onComplete: null
                };
            }

            const audioManager = this.getModule('audio-manager');
            if (audioManager && typeof audioManager.preloadMediaCues === 'function') {
                await audioManager.preloadMediaCues(metadata.cueMarkers || []);
            }

            const ttsData = await this.prepareSpeechMetadata(text, {
                sentenceId: id,
                blockId: metadata.blockId ?? null,
                turnId: metadata.turnId ?? null,
                blocking: true
            });

            console.log(`SentenceQueue: Prepared speech "${text.substring(0, 50)}..." - TTS duration: ${ttsData.duration}ms`);

            return {
                id,
                kind: metadata.type === 'heading' ? 'heading' : 'paragraph',
                text,
                turnId: metadata.turnId ?? null,
                blockId: metadata.blockId ?? null,
                gameId: metadata.gameId ?? null,
                paragraphIndex: metadata.paragraphIndex ?? null,
                isFirstParagraphInChapter: Boolean(metadata.isFirstParagraphInChapter),
                role: metadata.role || (metadata.type === 'heading' ? 'chapter-heading' : 'body'),
                dropCap: Boolean(metadata.dropCap),
                addTopSpace: Boolean(metadata.addTopSpace),
                cueMarkers: metadata.cueMarkers || [],
                status: 'ready',
                tts: {
                    duration: ttsData.duration,
                    provider: ttsData.handler,
                    audioData: ttsData.audioData || null,
                    play: ttsData.play,
                    stop: ttsData.stop,
                    enabled: ttsData.isTtsEnabled
                },
                animation: { wordTimings: [], cueTimings: [], totalDuration: 0 },
                element: null,
                onComplete: null
            };
        } catch (error) {
            console.error('SentenceQueue: Error preparing sentence:', error);
            throw error;
        }
    }

    /**
     * Prepare layout for a sentence
     * @param {string} text - Text to prepare layout for
     * @returns {Promise<Object>} - Layout data
     */
    async prepareLayout(text, metadata = {}) {
        const paragraphLayout = this.getModule('paragraph-layout');

        if (!paragraphLayout) {
            throw new Error("ParagraphLayout module not found");
        }

        try {
            if (document.fonts && document.fonts.ready) {
                await document.fonts.ready;
            }

            // Calculate layout with Knuth-Plass
            const storyElement = document.getElementById('story');
            if (!storyElement) {
                throw new Error("Story container not found");
            }

            // Get actual CSS values from the paragraph typography rule, not the
            // container. The measured font and rendered font must be identical.
            const containerWidth = storyElement.clientWidth;
            const probe = document.createElement('p');
            probe.style.visibility = 'hidden';
            probe.style.position = 'absolute';
            probe.style.left = '-8000px';
            probe.style.top = '-8000px';
            storyElement.appendChild(probe);
            const computedStyle = window.getComputedStyle(probe);
            const fontSize = parseFloat(computedStyle.fontSize);
            const lineHeight = parseFloat(computedStyle.lineHeight);
            const fontFamily = computedStyle.fontFamily;
            probe.remove();

            console.log(`SentenceQueue: Container metrics - width: ${containerWidth}px, fontSize: ${fontSize}px, lineHeight: ${lineHeight}px`);

            // Standard book indentation: no indent on the first chapter paragraph,
            // first-line indent on following paragraphs.
            const isHeading = metadata.type === 'heading' || metadata.role === 'chapter-heading' || metadata.role === 'section-heading';
            const dropCapLines = metadata.dropCap ? 2 : 0;
            const dropCapWidth = metadata.dropCap ? lineHeight * 1.45 : 0;
            const indentWidth = (isHeading || metadata.isFirstParagraphInChapter || metadata.addTopSpace) ? 0 : lineHeight * 1.5;
            const layoutText = metadata.layoutText || text;
            const layoutPlainText = metadata.dropCap ? this.extractDropCapText(layoutText) : layoutText;
            const measures = Array.isArray(metadata.measures) && metadata.measures.length > 0
                ? metadata.measures
                : isHeading
                ? [containerWidth]
                : metadata.dropCap
                ? [
                    Math.max(120, containerWidth - dropCapWidth),
                    Math.max(120, containerWidth - dropCapWidth),
                    containerWidth
                ]
                : [
                    Math.max(120, containerWidth - indentWidth),
                    containerWidth,
                    containerWidth
                ];
            const lineOffsets = Array.isArray(metadata.lineOffsets) && metadata.lineOffsets.length > 0
                ? metadata.lineOffsets
                : isHeading
                ? [0]
                : metadata.dropCap
                ? [
                    dropCapWidth,
                    dropCapWidth,
                    0
                ]
                : [
                    indentWidth,
                    0,
                    0
                ];

            console.log(`SentenceQueue: Layout calculation - indentWidth: ${indentWidth.toFixed(1)}px, measures: [${measures.map(m => m.toFixed(1)).join(', ')}], offsets: [${lineOffsets.map(m => m.toFixed(1)).join(', ')}]`);

            const layout = paragraphLayout.calculateLayout(layoutPlainText, {
                measures,
                fontSize: `${fontSize}px`,
                fontFamily,
                lineHeight: lineHeight / fontSize,
                lineHeightPx: lineHeight
            });

            if (!layout) {
                throw new Error('Paragraph layout calculation failed');
            }

            return {
                breaks: layout.breaks,
                nodes: layout.nodes,
                processedText: layout.processedText || text,
                sourceLayoutText: layoutText,
                measures,
                lineOffsets,
                indentWidth,
                imageWrap: metadata.imageWrap || null,
                dropCap: Boolean(metadata.dropCap),
                dropCapText: metadata.dropCap ? this.getDropCapText(layoutText) : '',
                dropCapLines,
                addTopSpace: Boolean(metadata.addTopSpace),
                role: metadata.role || (isHeading ? 'chapter-heading' : 'body'),
                align: isHeading ? 'center' : 'justify',
                fontSize: layout.fontSize,
                fontFamily: layout.fontFamily,
                lineHeight: layout.lineHeight,
                lineHeightPx: layout.lineHeightPx
            };
        } catch (error) {
            console.error('SentenceQueue: Error preparing layout:', error);
            throw error;
        }
    }

    shouldPauseAfterSentence(sentence) {
        if (sentence.kind !== 'paragraph' || this.shouldAutoplay()) {
            return false;
        }
        if (this.lastContinueAt >= (sentence.playbackStartedAt || 0)) {
            return false;
        }
        if (this.sentenceQueue.length <= 1 && this.inputMode === 'choice') {
            return false;
        }
        return this.sentenceQueue.length > 1;
    }

    shouldAutoplay() {
        const persistenceManager = this.getModule('persistence-manager');
        if (persistenceManager && typeof persistenceManager.getPreference === 'function') {
            return persistenceManager.getPreference('app', 'autoplay', this.autoplay) !== false;
        }
        return this.autoplay !== false;
    }

    waitForManualContinue(sentenceId) {
        document.documentElement.dataset.skippablePause = 'true';
        document.dispatchEvent(new CustomEvent('story:process-state', {
            detail: { state: 'paused', reason: 'autoplay-disabled', sentenceId }
        }));
        return new Promise(resolve => {
            let resolved = false;
            const finish = () => {
                if (resolved) return;
                resolved = true;
                delete document.documentElement.dataset.skippablePause;
                document.removeEventListener('ui:command', onCommand);
                document.dispatchEvent(new CustomEvent('story:process-state', {
                    detail: { state: 'playing-ready', reason: 'manual-continue', sentenceId }
                }));
                resolve();
            };
            const onCommand = (event) => {
                if (event.detail?.type === 'continue') {
                    finish();
                }
            };
            document.addEventListener('ui:command', onCommand);
        });
    }

    getCacheKey(item) {
        return `${item?.id || ''}:${item?.text || ''}`;
    }

    async getPreparedSentence(item) {
        const pending = this.prefetchingSpeech.get(this.getCacheKey(item));
        if (pending) {
            await pending.catch(() => null);
        }

        return this.prepareSentence(item);
    }

    prefetchAhead(maxLookahead = 4) {
        if (this.sentenceQueue.length <= 1) {
            document.dispatchEvent(new CustomEvent('story:process-state', {
                detail: { state: 'playing-ready', reason: 'no-prefetch-needed', sentenceId: this.sentenceQueue[0]?.id }
            }));
            console.log('Process state: playing-ready', { reason: 'no-prefetch-needed', sentenceId: this.sentenceQueue[0]?.id });
            return;
        }

        let started = 0;
        let spokenPrepared = 0;
        const limit = Math.min(this.sentenceQueue.length, maxLookahead + 1);

        for (let index = 1; index < limit; index += 1) {
            const nextItem = this.sentenceQueue[index];
            const nextCacheKey = this.getCacheKey(nextItem);
            if (this.prefetchingSpeech.has(nextCacheKey)) {
                if (this.isSpeechItem(nextItem)) spokenPrepared += 1;
                continue;
            }

            const state = this.isSpeechItem(nextItem) ? 'playing-generating' : 'playing-ready';
            document.dispatchEvent(new CustomEvent('story:process-state', {
                detail: { state, reason: 'prefetch-start', sentenceId: nextItem.id, queueIndex: index }
            }));
            console.log(`Process state: ${state}`, { reason: 'prefetch-start', sentenceId: nextItem.id, queueIndex: index });

            const promise = (this.isSpeechItem(nextItem)
                ? this.prepareSpeechMetadata(nextItem.text || '', {
                    sentenceId: nextItem.id,
                    blockId: nextItem.blockId ?? null,
                    turnId: nextItem.turnId ?? null,
                    queueIndex: index,
                    prefetch: true,
                    blocking: false
                })
                : Promise.resolve(null))
                .then(() => {
                    console.log('SentenceQueue: Prefetched queued speech/media', { sentenceId: nextItem.id, queueIndex: index });
                    document.dispatchEvent(new CustomEvent('story:process-state', {
                        detail: { state: 'playing-ready', reason: 'prefetch-complete', sentenceId: nextItem.id, queueIndex: index }
                    }));
                    console.log('Process state: playing-ready', { reason: 'prefetch-complete', sentenceId: nextItem.id, queueIndex: index });
                    return true;
                })
                .catch(err => {
                    console.warn('SentenceQueue: Prefetch failed:', err);
                    return null;
                })
                .finally(() => {
                    this.prefetchingSpeech.delete(nextCacheKey);
                });

            this.prefetchingSpeech.set(nextCacheKey, promise);
            started += 1;

            if (this.isSpeechItem(nextItem)) {
                spokenPrepared += 1;
            }

            if (spokenPrepared >= 1 && started >= 2) {
                break;
            }
        }

        if (started === 0) {
            document.dispatchEvent(new CustomEvent('story:process-state', {
                detail: { state: 'playing-ready', reason: 'prefetch-already-ready', sentenceId: this.sentenceQueue[0]?.id }
            }));
            console.log('Process state: playing-ready', { reason: 'prefetch-already-ready', sentenceId: this.sentenceQueue[0]?.id });
        }
    }

    isSpeechItem(item) {
        const type = item?.type || 'paragraph';
        return type === 'paragraph' || type === 'heading' || !['image', 'music'].includes(type);
    }

    getMediaPauseSeconds(sentence) {
        if (!sentence || !['image', 'music'].includes(sentence.kind)) {
            return 0;
        }

        const metadata = sentence.metadata || {};
        const configuredPause = this.readFirstFiniteNumber(
            metadata.leadInSeconds,
            metadata.leadIn,
            metadata.pause,
            metadata.delay,
            0
        );

        if (sentence.kind !== 'image') {
            return configuredPause;
        }

        const revealSeconds = Number(metadata.imageRevealSeconds || metadata.revealSeconds || 0.9);
        return Math.max(configuredPause, Number.isFinite(revealSeconds) ? revealSeconds : 0.9);
    }

    readFirstFiniteNumber(...values) {
        for (const value of values) {
            const number = Number(value);
            if (Number.isFinite(number)) {
                return Math.max(0, number);
            }
        }

        return 0;
    }

    waitForSkippableMediaPause(seconds, kind = 'media', sentenceId = null) {
        const duration = Math.max(0, Number(seconds) || 0) * 1000;
        if (duration <= 0) return Promise.resolve(false);

        const startedAt = performance.now();
        console.log(`SentenceQueue: Waiting ${seconds}s for ${kind} lead`, { sentenceId });
        document.documentElement.dataset.skippablePause = 'true';
        document.dispatchEvent(new CustomEvent('story:process-state', {
            detail: { state: 'playing-ready', reason: `${kind}-pause-start`, duration, sentenceId }
        }));

        return new Promise(resolve => {
            let finished = false;
            let timeoutId = null;

            const finish = (skipped, source = null) => {
                if (finished) return;
                finished = true;
                clearTimeout(timeoutId);
                document.removeEventListener('ui:command', onCommand);
                delete document.documentElement.dataset.skippablePause;
                const elapsedMs = Math.round(performance.now() - startedAt);
                console.log(`SentenceQueue: ${kind} lead ${skipped ? 'skipped' : 'complete'}`, { sentenceId, elapsedMs, source });
                document.dispatchEvent(new CustomEvent('story:process-state', {
                    detail: { state: 'playing-ready', reason: `${kind}-pause-${skipped ? 'skipped' : 'complete'}`, duration, elapsedMs, sentenceId }
                }));
                resolve(skipped);
            };

            const onCommand = (event) => {
                if (event.detail?.type === 'continue') {
                    finish(true, event.detail);
                }
            };

            document.addEventListener('ui:command', onCommand);
            timeoutId = setTimeout(() => finish(false), duration);
        });
    }

    /**
     * Extract words from layout nodes
     * @param {Array} nodes - Layout nodes from Knuth-Plass algorithm
     * @returns {Array<string>} - Array of words
     */
    extractWords(nodes) {
        if (!nodes || !Array.isArray(nodes)) {
            return [];
        }

        return nodes
            .filter(node => node.type === 'box')
            .map(node => node.value || '');
    }

    getDropCapText(text) {
        const plain = String(text || '').replace(/<[^>]+>/g, '');
        const match = plain.match(/^([“"']?[A-Za-zÀ-ÖØ-öø-ÿ])/u);
        return match ? match[1] : '';
    }

    extractDropCapText(text) {
        const dropCap = this.getDropCapText(text);
        if (!dropCap) return text;
        return String(text).replace(dropCap, '').trimStart();
    }

    /**
     * Calculate animation timing based on TTS duration
     * @param {Array<string>} words - Array of words to animate
     * @param {number} totalDuration - Total duration in milliseconds
     * @returns {Object} - Animation timing data
     */
    calculateAnimationTiming(words, totalDuration, cueMarkers = []) {
        if (!words || words.length === 0) {
            return {
                wordTimings: [],
                cueTimings: [],
                totalDuration: 0
            };
        }

        const totalChars = words.reduce((sum, word) => sum + word.length, 0);

        if (totalChars === 0) {
            return {
                wordTimings: words.map(word => ({ word, delay: 0, duration: 0 })),
                cueTimings: [],
                totalDuration: 0
            };
        }

        const msPerChar = totalDuration / totalChars;

        let currentDelay = 0;
        const wordTimings = words.map(word => {
            const duration = word.length * msPerChar;
            const timing = {
                word: word,
                delay: currentDelay,
                duration: duration
            };
            currentDelay += duration;
            return timing;
        });

        const cueTimings = (cueMarkers || []).map(cue => {
            const wordIndex = Math.max(0, Math.min(cue.wordIndex || 0, wordTimings.length - 1));
            const timing = wordTimings[wordIndex] || { delay: currentDelay };
            return {
                ...cue,
                delay: timing.delay
            };
        });

        return {
            wordTimings,
            cueTimings,
            totalDuration: Math.round(currentDelay)
        };
    }

    /**
     * Complete processing of a sentence
     * @param {Object} item - Queue item
     * @param {Object} result - Processing result
     */
    completeSentence(item, result) {
        // Remove from queue
        this.sentenceQueue.shift();
        
        // Call the original callback
        if (item.callback) {
            item.callback(result);
        }
        
        // Reset processing flag
        this.isProcessing = false;
        
        // Process next sentence if any
        if (this.sentenceQueue.length > 0) {
            this.processNextSentence();
        }
    }

    clear() {
        this.sentenceQueue = [];
        this.isProcessing = false;
        this.cancelGenerationRequests('sentence-queue-cleared');
        this.prefetchingSpeech.clear();
        document.dispatchEvent(new CustomEvent('tts:queue-empty', {
            detail: { reason: 'sentence-queue-cleared' }
        }));
        document.dispatchEvent(new CustomEvent('story:process-state', {
            detail: { state: 'ready', reason: 'sentence-queue-cleared' }
        }));
    }
}

// Create the singleton instance
const SentenceQueue = new SentenceQueueModule();

// Export the module
export { SentenceQueue };

// Register with the module registry
if (window.moduleRegistry) {
    window.moduleRegistry.register(SentenceQueue);
}

// Keep a reference in window for loader system
window.SentenceQueue = SentenceQueue;
