/**
 * Block-Aware Markdown Chunker
 *
 * Intelligently chunks streaming markdown content while preserving
 * code fence integrity. Never splits inside code blocks unless forced.
 */
import { EventEmitter } from 'events';
/**
 * Default chunker configuration
 */
export const DEFAULT_CHUNKER_CONFIG = {
    softMaxChars: 2000,
    hardMaxChars: 4000,
    preferredBreaks: ['\n\n', '\n', '. ', ', ', ' '],
    preserveCodeBlocks: true,
    addContinuationMarkers: false,
};
// ============================================================================
// Block State Tracker
// ============================================================================
/**
 * Create initial block state
 */
export function createBlockState() {
    return {
        inCodeBlock: false,
        fence: '',
        language: '',
        depth: 0,
    };
}
/**
 * Detect fence opening/closing in text
 */
export function detectFence(text, state) {
    const fencePositions = [];
    const newState = { ...state };
    // Match both ``` and ~~~ fences at start of line or after newline
    // Also match fences that might be at the end (without trailing newline)
    const fenceRegex = /(?:^|\n)(```|~~~)(\w*)?(?:\n|$)/g;
    let match;
    while ((match = fenceRegex.exec(text)) !== null) {
        const fence = match[1];
        const language = match[2] || '';
        const index = match.index + (match[0].startsWith('\n') ? 1 : 0);
        if (!newState.inCodeBlock) {
            // Opening fence
            newState.inCodeBlock = true;
            newState.fence = fence;
            newState.language = language;
            newState.depth++;
            fencePositions.push({
                index,
                isOpen: true,
                fence,
                language,
            });
        }
        else if (fence === newState.fence) {
            // Closing fence (must match opening fence type)
            newState.inCodeBlock = false;
            newState.depth--;
            fencePositions.push({
                index,
                isOpen: false,
                fence,
                language: '',
            });
        }
    }
    return { newState, fencePositions };
}
/**
 * Update block state with new content
 */
export function updateBlockState(text, state) {
    return detectFence(text, state).newState;
}
// ============================================================================
// Markdown Chunker
// ============================================================================
/**
 * Block-aware markdown chunker
 */
export class MarkdownChunker extends EventEmitter {
    config;
    buffer = '';
    blockState;
    chunks = [];
    constructor(config = {}) {
        super();
        this.config = { ...DEFAULT_CHUNKER_CONFIG, ...config };
        this.blockState = createBlockState();
    }
    /**
     * Add text to the buffer
     */
    write(text) {
        this.buffer += text;
        return this.processBuffer(false);
    }
    /**
     * Flush remaining buffer
     */
    flush() {
        const results = this.processBuffer(true);
        // Add any remaining content as final chunk
        if (this.buffer.length > 0) {
            results.push({
                content: this.buffer,
                forceSplit: false,
                fenceClosed: false,
            });
            this.buffer = '';
        }
        return results;
    }
    /**
     * Process the buffer and extract chunks
     */
    processBuffer(isFlush) {
        const results = [];
        while (this.buffer.length >= this.config.softMaxChars ||
            (isFlush && this.buffer.length > 0)) {
            const chunk = this.extractChunk();
            if (!chunk)
                break;
            results.push(chunk);
            this.chunks.push(chunk);
            this.emit('chunk', chunk);
            // If we're flushing and buffer is small, stop processing
            if (isFlush && this.buffer.length < this.config.softMaxChars) {
                break;
            }
        }
        return results;
    }
    /**
     * Extract a chunk from the buffer
     */
    extractChunk() {
        if (this.buffer.length === 0) {
            return null;
        }
        // Update block state for current buffer
        const { newState, fencePositions } = detectFence(this.buffer, this.blockState);
        // Find the best split point
        let splitIndex = this.findSplitPoint(fencePositions);
        // If we couldn't find a good split point and we're under soft max, wait for more data
        if (splitIndex === -1 && this.buffer.length < this.config.hardMaxChars) {
            return null;
        }
        // Force split if we're at hard max
        if (splitIndex === -1 || splitIndex > this.config.hardMaxChars) {
            return this.forceSplit();
        }
        // Extract the chunk
        const content = this.buffer.slice(0, splitIndex);
        this.buffer = this.buffer.slice(splitIndex);
        // Update state for the extracted content
        this.blockState = updateBlockState(content, this.blockState);
        return {
            content,
            forceSplit: false,
            fenceClosed: false,
        };
    }
    /**
     * Find the best split point
     */
    findSplitPoint(fencePositions) {
        const softMax = this.config.softMaxChars;
        // If we're in a code block and preserving them, try to find the closing fence
        if (this.blockState.inCodeBlock && this.config.preserveCodeBlocks) {
            // Find the first closing fence after the current position
            for (const pos of fencePositions) {
                if (!pos.isOpen && pos.index >= softMax * 0.5 && pos.index <= this.config.hardMaxChars) {
                    // Split right after the closing fence (include the newline)
                    const afterFence = pos.index + pos.fence.length;
                    const nextNewline = this.buffer.indexOf('\n', afterFence);
                    return nextNewline !== -1 ? nextNewline + 1 : afterFence;
                }
            }
        }
        // If not in a code block, find a natural break point
        if (!this.blockState.inCodeBlock) {
            for (const breakChar of this.config.preferredBreaks) {
                // Search backwards from soft max
                let searchStart = Math.min(softMax, this.buffer.length);
                let breakIndex = this.buffer.lastIndexOf(breakChar, searchStart);
                // Make sure we're not too close to the start
                if (breakIndex > softMax * 0.3) {
                    return breakIndex + breakChar.length;
                }
            }
        }
        return -1;
    }
    /**
     * Force split the buffer (used when hard max is reached)
     */
    forceSplit() {
        const hardMax = this.config.hardMaxChars;
        let content;
        let fenceClosed = false;
        let reopenFence;
        if (this.blockState.inCodeBlock && this.config.preserveCodeBlocks) {
            // We're in a code block and must split
            // Close the fence, split, and prepare to reopen
            const splitPoint = Math.min(hardMax - this.blockState.fence.length - 1, this.buffer.length);
            content = this.buffer.slice(0, splitPoint);
            // Close the fence
            content += '\n' + this.blockState.fence;
            fenceClosed = true;
            reopenFence = this.blockState.fence + this.blockState.language;
            this.buffer = this.buffer.slice(splitPoint);
            // Prepend the reopening fence to the buffer
            this.buffer = reopenFence + '\n' + this.buffer;
        }
        else {
            // Not in a code block, just split at hard max
            content = this.buffer.slice(0, hardMax);
            this.buffer = this.buffer.slice(hardMax);
        }
        // Update state
        this.blockState = updateBlockState(content, this.blockState);
        return {
            content,
            forceSplit: true,
            fenceClosed,
            reopenFence,
        };
    }
    /**
     * Get current block state
     */
    getBlockState() {
        return { ...this.blockState };
    }
    /**
     * Get all chunks generated so far
     */
    getChunks() {
        return [...this.chunks];
    }
    /**
     * Get current buffer content
     */
    getBuffer() {
        return this.buffer;
    }
    /**
     * Reset the chunker
     */
    reset() {
        this.buffer = '';
        this.blockState = createBlockState();
        this.chunks = [];
    }
}
// ============================================================================
// Convenience Functions
// ============================================================================
/**
 * Chunk a complete markdown string
 */
export function chunkMarkdown(text, config) {
    const chunker = new MarkdownChunker(config);
    chunker.write(text);
    return chunker.flush();
}
/**
 * Check if text contains unclosed code blocks
 */
export function hasUnclosedCodeBlock(text) {
    const state = updateBlockState(text, createBlockState());
    return state.inCodeBlock;
}
/**
 * Count code blocks in text
 */
export function countCodeBlocks(text) {
    const { fencePositions } = detectFence(text, createBlockState());
    let open = 0;
    let closed = 0;
    for (const pos of fencePositions) {
        if (pos.isOpen)
            open++;
        else
            closed++;
    }
    return { open, closed };
}
/**
 * Fix unclosed code blocks in text
 */
export function fixUnclosedCodeBlocks(text) {
    let state = createBlockState();
    state = updateBlockState(text, state);
    if (state.inCodeBlock) {
        return text + '\n' + state.fence;
    }
    return text;
}
/**
 * Create a streaming chunker with callback
 */
export function createStreamingChunker(onChunk, config) {
    const chunker = new MarkdownChunker(config);
    chunker.on('chunk', onChunk);
    return {
        write: (text) => chunker.write(text),
        flush: () => chunker.flush(),
        reset: () => chunker.reset(),
    };
}
//# sourceMappingURL=markdown-chunker.js.map