import { Message, ReasoningBlock, TextBlock, ToolUseBlock, } from '../types/messages.js';
import { CitationsBlock } from '../types/citations.js';
import { ModelContentBlockDeltaEvent, ModelContentBlockStartEvent, ModelContentBlockStopEvent, ModelMessageStartEvent, ModelMessageStopEvent, ModelMetadataEvent, ModelRedactionEvent, } from './streaming.js';
import { MaxTokensError, ModelError, normalizeError } from '../errors.js';
import { logger } from '../logging/logger.js';
import { getContextWindowLimit } from './defaults.js';
/**
 * Resolves model metadata fields on a config object from built-in lookup tables
 * when not explicitly set. Explicit values pass through unchanged.
 *
 * @internal
 * @param config - The stored model config
 * @param modelId - The model ID to look up
 * @returns A new config with resolved metadata, or the original config if nothing to resolve
 */
export function resolveConfigMetadata(config, modelId) {
    if (config.contextWindowLimit !== undefined)
        return config;
    const limit = getContextWindowLimit(modelId);
    if (limit === undefined)
        return config;
    return { ...config, contextWindowLimit: limit };
}
class CitationAccumulator {
    citations = [];
    content = [];
    push(citations, content) {
        this.citations.push(...citations);
        this.content.push(...content);
    }
    hasData() {
        return this.citations.length > 0;
    }
    reset() {
        this.citations = [];
        this.content = [];
    }
}
/**
 * Base abstract class for model providers.
 * Defines the contract that all model provider implementations must follow.
 *
 * Model providers handle communication with LLM APIs and implement streaming
 * responses using async iterables.
 *
 * @typeParam T - Model configuration type extending BaseModelConfig
 */
export class Model {
    /**
     * The model ID from the current configuration, if configured.
     */
    get modelId() {
        return this.getConfig().modelId;
    }
    /**
     * Whether this model manages conversation state server-side.
     *
     * When `true`, the server tracks conversation context across turns, so the SDK
     * sends only the latest message instead of the full history. After each invocation,
     * the agent's local message history is cleared automatically.
     *
     * Model providers that support server-side state management should override this
     * to return `true`.
     *
     * @returns `false` by default
     */
    get stateful() {
        return false;
    }
    /**
     * Count tokens for the given input before sending to the model.
     *
     * Used for proactive context management (e.g., triggering compression at a threshold).
     * The base implementation uses a character-based heuristic (chars/4 for text, chars/2 for JSON).
     *
     * Subclasses should override this method to use native token counting APIs
     * (e.g., Bedrock CountTokens, Anthropic countTokens, Gemini countTokens)
     * for improved accuracy, falling back to `super.countTokens()` on API failure.
     *
     * @param messages - Array of conversation messages to count tokens for
     * @param options - Optional options containing system prompt and tool specs
     * @returns Total input token count
     */
    async countTokens(messages, options) {
        return estimateTokensHeuristic(messages, options);
    }
    /**
     * Converts event data to event class representation
     *
     * @param event_data - Interface representation of event
     * @returns Class representation of event
     */
    _convert_to_class_event(event_data) {
        switch (event_data.type) {
            case 'modelMessageStartEvent':
                return new ModelMessageStartEvent(event_data);
            case 'modelContentBlockStartEvent':
                return new ModelContentBlockStartEvent(event_data);
            case 'modelContentBlockDeltaEvent':
                return new ModelContentBlockDeltaEvent(event_data);
            case 'modelContentBlockStopEvent':
                return new ModelContentBlockStopEvent(event_data);
            case 'modelMessageStopEvent':
                return new ModelMessageStopEvent(event_data);
            case 'modelMetadataEvent':
                return new ModelMetadataEvent(event_data);
            case 'modelRedactionEvent':
                return new ModelRedactionEvent(event_data);
            default:
                throw new Error(`Unsupported event type: ${event_data.type}`);
        }
    }
    /**
     * Streams a conversation with aggregated content blocks and messages.
     * Returns an async generator that yields streaming events and content blocks, and returns the final message with stop reason and optional metadata.
     *
     * This method enhances the basic stream() by collecting streaming events into complete
     * ContentBlock and Message objects, which are needed by the agentic loop for tool execution
     * and conversation management.
     *
     * The method yields:
     * - ModelStreamEvent - Original streaming events (passed through)
     * - ContentBlock - Complete content block (emitted when block completes)
     *
     * The method returns:
     * - StreamAggregatedResult containing the complete message, stop reason, and optional metadata
     *
     * All exceptions thrown from this method are wrapped in ModelError to provide
     * a consistent error type for model-related errors. Specific error subtypes like
     * ContextWindowOverflowError, ModelThrottledError, and MaxTokensError are preserved.
     *
     * @param messages - Array of conversation messages
     * @param options - Optional streaming configuration
     * @returns Async generator yielding ModelStreamEvent | ContentBlock and returning a StreamAggregatedResult
     * @throws ModelError - Base class for all model-related errors
     * @throws ContextWindowOverflowError - When input exceeds the model's context window
     * @throws ModelThrottledError - When the model provider throttles requests
     * @throws MaxTokensError - When the model reaches its maximum token limit
     */
    async *streamAggregated(messages, options) {
        try {
            // State maintained in closure
            let messageRole = null;
            const contentBlocks = [];
            let accumulatedText = '';
            let accumulatedToolInput = '';
            let toolName = '';
            let toolUseId = '';
            let toolReasoningSignature = '';
            let accumulatedReasoning = {};
            const accumulatedCitations = new CitationAccumulator();
            let stoppedMessage = null;
            let finalStopReason = null;
            let metadata = undefined;
            let redactionMessage = undefined;
            let toolInputParseError = undefined;
            for await (const event_data of this.stream(messages, options)) {
                const event = this._convert_to_class_event(event_data);
                yield event; // Pass through immediately
                // Aggregation logic based on event type
                switch (event.type) {
                    case 'modelMessageStartEvent':
                        messageRole = event.role;
                        contentBlocks.length = 0; // Reset
                        break;
                    case 'modelContentBlockStartEvent':
                        if (event.start?.type === 'toolUseStart') {
                            toolName = event.start.name;
                            toolUseId = event.start.toolUseId;
                            toolReasoningSignature = event.start.reasoningSignature ?? '';
                        }
                        accumulatedToolInput = '';
                        accumulatedText = '';
                        accumulatedReasoning = {};
                        accumulatedCitations.reset();
                        break;
                    case 'modelContentBlockDeltaEvent': {
                        switch (event.delta.type) {
                            case 'textDelta':
                                accumulatedText += event.delta.text;
                                break;
                            case 'toolUseInputDelta':
                                accumulatedToolInput += event.delta.input;
                                break;
                            case 'reasoningContentDelta':
                                if (event.delta.text)
                                    accumulatedReasoning.text = (accumulatedReasoning.text ?? '') + event.delta.text;
                                if (event.delta.signature)
                                    accumulatedReasoning.signature = event.delta.signature;
                                if (event.delta.redactedContent)
                                    accumulatedReasoning.redactedContent = event.delta.redactedContent;
                                break;
                            case 'citationsDelta':
                                accumulatedCitations.push(event.delta.citations, event.delta.content);
                                break;
                        }
                        break;
                    }
                    case 'modelContentBlockStopEvent': {
                        // Finalize and emit complete ContentBlock
                        let block;
                        try {
                            if (toolUseId) {
                                block = new ToolUseBlock({
                                    name: toolName,
                                    toolUseId: toolUseId,
                                    input: accumulatedToolInput ? JSON.parse(accumulatedToolInput) : {},
                                    ...(toolReasoningSignature && { reasoningSignature: toolReasoningSignature }),
                                });
                                toolUseId = ''; // Reset
                                toolName = '';
                                toolReasoningSignature = '';
                            }
                            else if (Object.keys(accumulatedReasoning).length > 0) {
                                block = new ReasoningBlock({
                                    ...accumulatedReasoning,
                                });
                                accumulatedReasoning = {}; // Reset after creating reasoning block
                            }
                            else if (accumulatedCitations.hasData()) {
                                block = new CitationsBlock({
                                    citations: accumulatedCitations.citations,
                                    content: accumulatedCitations.content,
                                });
                                accumulatedCitations.reset();
                            }
                            else {
                                block = new TextBlock(accumulatedText);
                            }
                            contentBlocks.push(block);
                            yield block;
                        }
                        catch (e) {
                            if (e instanceof SyntaxError) {
                                logger.error('unable to parse tool input JSON', e);
                                toolInputParseError = e;
                            }
                        }
                        break;
                    }
                    case 'modelMessageStopEvent':
                        // Store message and stop reason
                        if (messageRole) {
                            stoppedMessage = new Message({
                                role: messageRole,
                                content: [...contentBlocks],
                            });
                            finalStopReason = event.stopReason;
                        }
                        break;
                    case 'modelMetadataEvent':
                        // Store metadata, keeping the last one if multiple events occur
                        metadata = event;
                        break;
                    case 'modelRedactionEvent':
                        // Handle content redaction from guardrails
                        if (event.inputRedaction) {
                            // Store redaction message for agent to handle input message redaction
                            redactionMessage = event.inputRedaction.replaceContent;
                        }
                        if (event.outputRedaction) {
                            // Update output message directly with redacted content
                            // Redaction event comes after modelMessageStopEvent, so we overwrite stoppedMessage
                            stoppedMessage = new Message({
                                role: 'assistant',
                                content: [new TextBlock(event.outputRedaction.replaceContent)],
                            });
                        }
                        break;
                    default:
                        break;
                }
            }
            if (!stoppedMessage || !finalStopReason) {
                // If we exit the loop without completing a message or stop reason, throw an error
                throw new ModelError('Stream ended without completing a message', toolInputParseError ? { cause: toolInputParseError } : undefined);
            }
            // Attach metadata after redaction so it applies to the final message.
            const messageMetadata = {
                ...(metadata?.usage !== undefined && { usage: metadata.usage }),
                ...(metadata?.metrics !== undefined && { metrics: metadata.metrics }),
            };
            if (Object.keys(messageMetadata).length > 0) {
                stoppedMessage = new Message({
                    role: stoppedMessage.role,
                    content: stoppedMessage.content,
                    metadata: messageMetadata,
                });
            }
            // Handle stop reason
            if (finalStopReason === 'maxTokens') {
                throw new MaxTokensError('Model reached maximum token limit. This is an unrecoverable state that requires intervention.', stoppedMessage);
            }
            if (toolInputParseError !== undefined) {
                throw new ModelError('unable to parse tool input JSON', { cause: toolInputParseError });
            }
            // Return the final message with stop reason and optional metadata
            const result = {
                message: stoppedMessage,
                stopReason: finalStopReason,
            };
            if (metadata !== undefined) {
                result.metadata = metadata;
            }
            if (redactionMessage !== undefined) {
                result.redaction = { userMessage: redactionMessage };
            }
            return result;
        }
        catch (error) {
            // Wrap non-ModelError errors in ModelError
            if (error instanceof ModelError) {
                throw error;
            }
            const normalizedError = normalizeError(error);
            throw new ModelError(normalizedError.message, { cause: error });
        }
    }
}
/**
 * Estimate tokens for a content block using character-based heuristics.
 *
 * @param block - Content block to estimate tokens for
 * @returns Estimated token count
 */
function estimateContentBlockTokens(block) {
    let total = 0;
    switch (block.type) {
        case 'textBlock':
            total += heuristicText(block.text);
            break;
        case 'toolUseBlock':
            total += heuristicText(block.name);
            total += heuristicJson(block.input);
            break;
        case 'toolResultBlock':
            for (const item of block.content) {
                if (item.type === 'textBlock') {
                    total += heuristicText(item.text);
                }
                else if (item.type === 'jsonBlock') {
                    total += heuristicJson(item.json);
                }
            }
            break;
        case 'reasoningBlock':
            if (block.text)
                total += heuristicText(block.text);
            break;
        case 'guardContentBlock':
            if (block.text)
                total += heuristicText(block.text.text);
            break;
        case 'citationsBlock':
            for (const item of block.content) {
                if ('text' in item)
                    total += heuristicText(item.text);
            }
            break;
        default:
            break;
    }
    return total;
}
/**
 * Estimate token count using character-based heuristics (text: chars/4, JSON: chars/2).
 * Dependency-free fallback used by the base Model class.
 */
function estimateTokensHeuristic(messages, options) {
    let total = 0;
    if (options?.systemPrompt) {
        if (typeof options.systemPrompt === 'string') {
            total += heuristicText(options.systemPrompt);
        }
        else {
            for (const block of options.systemPrompt) {
                if (block.type === 'textBlock')
                    total += heuristicText(block.text);
                else if (block.type === 'guardContentBlock' && block.text)
                    total += heuristicText(block.text.text);
            }
        }
    }
    for (const message of messages) {
        for (const block of message.content) {
            total += estimateContentBlockTokens(block);
        }
    }
    if (options?.toolSpecs) {
        for (const spec of options.toolSpecs) {
            total += heuristicJson(spec);
        }
    }
    return total;
}
function heuristicText(text) {
    return Math.ceil(text.length / 4);
}
function heuristicJson(obj) {
    try {
        return Math.ceil(JSON.stringify(obj).length / 2);
    }
    catch {
        logger.debug('unable to serialize object for token estimation, skipping');
        return 0;
    }
}
//# sourceMappingURL=model.js.map