/**
 * Shared utilities for Anthropic SDK-based drivers.
 *
 * Used by both the native AnthropicDriver (drivers/src/anthropic/) and the
 * VertexAI Claude pathway (drivers/src/vertexai/models/claude.ts).  Both use
 * the same Anthropic Messages API surface — the only difference is the client
 * (Anthropic vs AnthropicVertex) and how auth is wired up.
 */

import type Anthropic from '@anthropic-ai/sdk';
import {
    AnthropicError,
    APIConnectionError,
    APIConnectionTimeoutError,
    APIError,
    APIUserAbortError,
    AuthenticationError,
    BadRequestError,
    ConflictError,
    InternalServerError,
    NotFoundError,
    PermissionDeniedError,
    RateLimitError,
    UnprocessableEntityError,
} from '@anthropic-ai/sdk/error';
import type {
    ContentBlock,
    ContentBlockParam,
    DocumentBlockParam,
    ImageBlockParam,
    Message,
    MessageParam,
    TextBlockParam,
    ToolResultBlockParam,
} from '@anthropic-ai/sdk/resources/index.js';
import type { MessageStreamParams } from '@anthropic-ai/sdk/resources/index.mjs';
import type {
    MessageCreateParamsBase,
    RawMessageStreamEvent,
} from '@anthropic-ai/sdk/resources/messages.js';
import type AnthropicVertex from '@anthropic-ai/vertex-sdk';
import { getClaudeMaxTokensLimit } from '@llumiverse/common';
import {
    type Completion,
    type CompletionChunkObject,
    type CompletionResult,
    type ExecutionOptions,
    type ExecutionTokenUsage,
    getConversationMeta,
    incrementConversationTurn,
    type JSONObject,
    LlumiverseError,
    type LlumiverseErrorContext,
    PromptRole,
    type PromptSegment,
    readStreamAsBase64,
    readStreamAsString,
    type StatelessExecutionOptions,
    stripBase64ImagesFromConversation,
    stripHeartbeatsFromConversation,
    type ToolUse,
    truncateLargeTextInConversation,
} from '@llumiverse/core';
import { asyncMap } from '@llumiverse/core/async';
import { resolveClaudeThinking } from './claude-thinking.js';

// ============================================================================
// Types
// ============================================================================

export interface ClaudePrompt {
    messages: MessageParam[];
    system?: TextBlockParam[];
}

export interface AnthropicUsageLike {
    input_tokens: number;
    output_tokens: number;
    cache_read_input_tokens?: number | null;
    cache_creation_input_tokens?: number | null;
}

/**
 * Duck-typed options interface accepted by the shared Claude utilities.
 * Both `AnthropicClaudeOptions` and `VertexAIClaudeOptions` satisfy this structurally.
 */
export interface ClaudeBaseOptions {
    _option_id?: string;
    max_tokens?: number;
    temperature?: number;
    top_p?: number;
    top_k?: number;
    stop_sequence?: string[];
    effort?: string;
    thinking_budget_tokens?: number;
    include_thoughts?: boolean;
    cache_enabled?: boolean;
    cache_ttl?: string;
}

interface RequestOptions {
    headers?: Record<string, string>;
}

type ClaudeTool = NonNullable<MessageCreateParamsBase['tools']>[number];

// ============================================================================
// Token usage
// ============================================================================

export function anthropicUsageToTokenUsage(usage: AnthropicUsageLike): ExecutionTokenUsage {
    const cacheRead = usage.cache_read_input_tokens ?? 0;
    const cacheWrite = usage.cache_creation_input_tokens ?? 0;
    return {
        prompt_new: usage.input_tokens,
        prompt: usage.input_tokens + cacheRead + cacheWrite,
        result: usage.output_tokens,
        total: usage.input_tokens + usage.output_tokens + cacheRead + cacheWrite,
        prompt_cached: usage.cache_read_input_tokens ?? undefined,
        prompt_cache_write: usage.cache_creation_input_tokens ?? undefined,
    };
}

// ============================================================================
// Finish reason
// ============================================================================

export function claudeFinishReason(reason: string | undefined): string | undefined {
    if (!reason) return undefined;
    switch (reason) {
        case 'end_turn': return 'stop';
        case 'max_tokens': return 'length';
        default: return reason; // stop_sequence, tool_use
    }
}

// ============================================================================
// Content extraction
// ============================================================================

export function collectClaudeTools(content: ContentBlock[]): ToolUse[] | undefined {
    const out: ToolUse[] = [];
    for (const block of content) {
        if (block.type === 'tool_use') {
            out.push({
                id: block.id,
                tool_name: block.name,
                tool_input: block.input as JSONObject,
            });
        }
    }
    return out.length > 0 ? out : undefined;
}

export function collectAllTextContent(content: ContentBlock[], includeThoughts = false): string {
    const textParts: string[] = [];

    if (includeThoughts) {
        for (const block of content) {
            if (block.type === 'thinking' && block.thinking) {
                textParts.push(block.thinking);
            } else if (block.type === 'redacted_thinking' && block.data) {
                textParts.push(`[Redacted thinking: ${block.data}]`);
            }
        }
        if (textParts.length > 0) {
            textParts.push('');
        }
    }

    for (const block of content) {
        if (block.type === 'text' && block.text) {
            textParts.push(block.text);
        }
    }

    return textParts.join('\n');
}

// ============================================================================
// Max tokens
// ============================================================================

export function claudeMaxTokens(option: StatelessExecutionOptions): number {
    const modelOptions = option.model_options as ClaudeBaseOptions | undefined;
    if (modelOptions && typeof modelOptions.max_tokens === 'number') {
        return modelOptions.max_tokens;
    }
    let maxSupportedTokens = getClaudeMaxTokensLimit(option.model);
    // Claude 3.7 supports up to 128k with a beta header; default to 64k when no budget is set.
    if (option.model.includes('claude-3-7-sonnet') && (modelOptions?.thinking_budget_tokens ?? 0) < 48000) {
        maxSupportedTokens = 64000;
    }
    return maxSupportedTokens;
}

// ============================================================================
// File / multimodal block helpers
// ============================================================================

async function collectFileBlocks(segment: PromptSegment, restrictedTypes: true): Promise<Array<TextBlockParam | ImageBlockParam>>;
async function collectFileBlocks(segment: PromptSegment, restrictedTypes?: false): Promise<ContentBlockParam[]>;
async function collectFileBlocks(segment: PromptSegment, restrictedTypes = false): Promise<ContentBlockParam[]> {
    const contentBlocks: ContentBlockParam[] = [];

    for (const file of segment.files || []) {
        if (file.mime_type?.startsWith('image/')) {
            const allowedTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
            if (!allowedTypes.includes(file.mime_type)) {
                throw new Error(`Unsupported image type: ${file.mime_type}`);
            }
            const mimeType = String(file.mime_type) as 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp';
            contentBlocks.push({
                type: 'image',
                source: {
                    type: 'base64',
                    data: await readStreamAsBase64(await file.getStream()),
                    media_type: mimeType,
                },
            } satisfies ImageBlockParam);
        } else if (!restrictedTypes) {
            if (file.mime_type === 'application/pdf') {
                contentBlocks.push({
                    title: file.name,
                    type: 'document',
                    source: {
                        type: 'base64',
                        data: await readStreamAsBase64(await file.getStream()),
                        media_type: 'application/pdf',
                    },
                } satisfies DocumentBlockParam);
            } else if (file.mime_type?.startsWith('text/')) {
                contentBlocks.push({
                    title: file.name,
                    type: 'document',
                    source: {
                        type: 'text',
                        data: await readStreamAsString(await file.getStream()),
                        media_type: 'text/plain',
                    },
                } satisfies DocumentBlockParam);
            }
        }
    }

    return contentBlocks;
}

// ============================================================================
// Prompt formatting (PromptSegment[] → ClaudePrompt)
// ============================================================================

export async function formatClaudePrompt(segments: PromptSegment[], options: ExecutionOptions): Promise<ClaudePrompt> {
    let system: TextBlockParam[] | undefined = segments
        .filter((s) => s.role === PromptRole.system)
        .map((s) => ({ text: s.content, type: 'text' as const }));

    if (options.result_schema) {
        const schemaText = options.tools && options.tools.length > 0
            ? 'When not calling tools, the answer must be a JSON object using the following JSON Schema:\n' + JSON.stringify(options.result_schema)
            : 'The answer must be a JSON object using the following JSON Schema:\n' + JSON.stringify(options.result_schema);
        system.push({ text: schemaText, type: 'text' as const });
    }

    let messages: MessageParam[] = [];
    const safetyMessages: MessageParam[] = [];

    for (const segment of segments) {
        if (segment.role === PromptRole.system) continue;

        if (segment.role === PromptRole.tool) {
            if (!segment.tool_use_id) {
                throw new Error('Tool prompt segment must have a tool use ID');
            }
            const contentBlocks: Array<TextBlockParam | ImageBlockParam> = [];
            if (segment.content) {
                contentBlocks.push({ type: 'text', text: segment.content } satisfies TextBlockParam);
            }
            contentBlocks.push(...(await collectFileBlocks(segment, true)));
            messages.push({
                role: 'user',
                content: [{
                    type: 'tool_result',
                    tool_use_id: segment.tool_use_id,
                    content: contentBlocks,
                } satisfies ToolResultBlockParam],
            });
        } else {
            const contentBlocks: ContentBlockParam[] = [];
            if (segment.content) {
                contentBlocks.push({ type: 'text', text: segment.content } satisfies TextBlockParam);
            }
            contentBlocks.push(...(await collectFileBlocks(segment, false)));
            if (contentBlocks.length === 0) continue;

            const messageParam: MessageParam = {
                role: segment.role === PromptRole.assistant ? 'assistant' : 'user',
                content: contentBlocks,
            };

            if (segment.role === PromptRole.safety) {
                safetyMessages.push(messageParam);
            } else {
                messages.push(messageParam);
            }
        }
    }

    messages = messages.concat(safetyMessages);
    if (system && system.length === 0) system = undefined;

    return { messages, system };
}

// ============================================================================
// Conversation management
// ============================================================================

export function createPromptFromResponse(response: Message): ClaudePrompt {
    return {
        messages: [{ role: response.role, content: response.content }],
        system: undefined,
    };
}

export function mergeConsecutiveUserMessages(messages: MessageParam[]): MessageParam[] {
    if (messages.length === 0) return [];

    const needsMerging = messages.some((msg, i) =>
        i < messages.length - 1 && msg.role === 'user' && messages[i + 1].role === 'user'
    );
    if (!needsMerging) return messages;

    const result: MessageParam[] = [];
    let i = 0;
    while (i < messages.length) {
        const current = messages[i];
        if (current.role === 'user') {
            const mergedContent: MessageParam['content'] = [];
            while (i < messages.length && messages[i].role === 'user') {
                const userMsg = messages[i];
                if (Array.isArray(userMsg.content)) {
                    mergedContent.push(...userMsg.content);
                } else if (typeof userMsg.content === 'string') {
                    mergedContent.push({ type: 'text', text: userMsg.content });
                }
                i++;
            }
            result.push({ role: 'user', content: mergedContent });
        } else {
            result.push(current);
            i++;
        }
    }
    return result;
}

export function sanitizeMessages(messages: MessageParam[]): MessageParam[] {
    const result: MessageParam[] = [];
    for (const message of messages) {
        if (typeof message.content === 'string') {
            if (message.content.trim()) result.push(message);
            continue;
        }
        const filteredContent = message.content.filter((block) => {
            if (block.type === 'text') return block.text && block.text.trim().length > 0;
            return true;
        });
        if (filteredContent.length > 0) {
            result.push({ ...message, content: filteredContent });
        }
    }
    return result;
}

export function fixOrphanedToolUse(messages: MessageParam[]): MessageParam[] {
    if (messages.length < 2) return messages;
    const result: MessageParam[] = [];
    for (let i = 0; i < messages.length; i++) {
        const current = messages[i];
        result.push(current);

        if (current.role === 'assistant' && Array.isArray(current.content)) {
            const toolUseBlocks = current.content.filter(
                (block): block is ContentBlockParam & { type: 'tool_use'; id: string; name: string } =>
                    block.type === 'tool_use'
            );

            if (toolUseBlocks.length > 0) {
                const nextMessage = messages[i + 1];

                if (nextMessage && nextMessage.role === 'user' && Array.isArray(nextMessage.content)) {
                    const toolResultIds = new Set(
                        nextMessage.content
                            .filter((block): block is ToolResultBlockParam => block.type === 'tool_result')
                            .map((block) => block.tool_use_id)
                    );
                    const orphaned = toolUseBlocks.filter((block) => !toolResultIds.has(block.id));
                    if (orphaned.length > 0) {
                        const syntheticResults: ToolResultBlockParam[] = orphaned.map((block) => ({
                            type: 'tool_result',
                            tool_use_id: block.id,
                            content: `[Tool interrupted: The user stopped the operation before "${block.name}" could execute.]`,
                        }));
                        messages[i + 1] = { ...nextMessage, content: [...syntheticResults, ...nextMessage.content] };
                    }
                } else if (nextMessage && nextMessage.role === 'user') {
                    const syntheticResults: ToolResultBlockParam[] = toolUseBlocks.map((block) => ({
                        type: 'tool_result',
                        tool_use_id: block.id,
                        content: `[Tool interrupted: The user stopped the operation before "${block.name}" could execute.]`,
                    }));
                    const textContent: TextBlockParam = typeof nextMessage.content === 'string'
                        ? { type: 'text', text: nextMessage.content }
                        : { type: 'text', text: '' };
                    messages[i + 1] = { role: 'user', content: [...syntheticResults, textContent] };
                }
            }
        }
    }
    return result;
}

export function updateClaudeConversation(conversation: ClaudePrompt | undefined | null, prompt: ClaudePrompt): ClaudePrompt {
    const baseSystemMessages = conversation?.system || [];
    const baseMessages = conversation?.messages || [];
    const system = baseSystemMessages.concat(prompt.system || []);
    const combined = sanitizeMessages(baseMessages.concat(prompt.messages || []));
    const mergedMessages = mergeConsecutiveUserMessages(combined);
    return {
        messages: mergedMessages,
        system: system.length > 0 ? system : undefined,
    };
}

export function claudeMessagesContainToolBlocks(messages: MessageParam[]): boolean {
    for (const msg of messages) {
        if (!Array.isArray(msg.content)) continue;
        for (const block of msg.content) {
            if (typeof block === 'object' && block !== null && 'type' in block) {
                if (block.type === 'tool_use' || block.type === 'tool_result') return true;
            }
        }
    }
    return false;
}

export function convertClaudeToolBlocksToText(messages: MessageParam[]): MessageParam[] {
    return messages.map((msg) => {
        if (!Array.isArray(msg.content)) return msg;
        let hasToolBlocks = false;
        for (const block of msg.content) {
            if (typeof block === 'object' && block !== null && 'type' in block &&
                (block.type === 'tool_use' || block.type === 'tool_result')) {
                hasToolBlocks = true;
                break;
            }
        }
        if (!hasToolBlocks) return msg;

        const newContent: MessageParam['content'] = [];
        for (const block of msg.content) {
            if (typeof block === 'string') { newContent.push(block); continue; }
            if (block.type === 'tool_use') {
                const inputStr = block.input ? JSON.stringify(block.input) : '';
                const truncated = inputStr.length > 500 ? inputStr.substring(0, 500) + '...' : inputStr;
                (newContent as Array<{ type: 'text'; text: string }>).push({ type: 'text', text: `[Tool call: ${block.name}(${truncated})]` });
            } else if (block.type === 'tool_result') {
                let resultStr = 'No content';
                if (typeof block.content === 'string') {
                    resultStr = block.content.length > 500 ? block.content.substring(0, 500) + '...' : block.content;
                } else if (Array.isArray(block.content)) {
                    const texts = block.content
                        .filter((c): c is { type: 'text'; text: string } => c.type === 'text')
                        .map((c) => (c.text.length > 500 ? c.text.substring(0, 500) + '...' : c.text));
                    resultStr = texts.join('\n') || 'No text content';
                }
                (newContent as Array<{ type: 'text'; text: string }>).push({ type: 'text', text: `[Tool result: ${resultStr}]` });
            } else {
                newContent.push(block as ContentBlockParam);
            }
        }
        return { ...msg, content: newContent };
    });
}

// ============================================================================
// Cache control stripping
// ============================================================================

function stripClaudeCacheControlFromBlock<T extends ContentBlockParam>(block: T): T {
    if (typeof block === 'object' && block !== null && 'cache_control' in block) {
        const { cache_control: _cc, ...rest } = block as T & { cache_control: unknown };
        return rest as T;
    }
    return block;
}

function stripClaudeCacheControlFromMessages(messages: MessageParam[]): MessageParam[] {
    return messages.map((msg) => {
        if (!Array.isArray(msg.content)) return msg;
        return { ...msg, content: msg.content.map(stripClaudeCacheControlFromBlock) };
    });
}

function stripClaudeCacheControlFromSystem(system?: TextBlockParam[]): TextBlockParam[] | undefined {
    if (!system) return undefined;
    return system.map(stripClaudeCacheControlFromBlock);
}

function stripClaudeCacheControlFromTools(
    tools?: MessageCreateParamsBase['tools']
): MessageCreateParamsBase['tools'] | undefined {
    if (!tools) return undefined;
    return tools.map((tool) => {
        if ('cache_control' in tool) {
            const { cache_control: _cc, ...rest } = tool as ClaudeTool & { cache_control: unknown };
            return rest as ClaudeTool;
        }
        return tool;
    });
}

// ============================================================================
// Payload builder
// ============================================================================

export function getClaudePayload(
    options: ExecutionOptions,
    prompt: ClaudePrompt
): { payload: MessageCreateParamsBase; requestOptions: RequestOptions | undefined } {
    const modelName = options.model;
    const model_options = options.model_options as ClaudeBaseOptions | undefined;

    let requestOptions: RequestOptions | undefined;
    if (modelName.includes('claude-3-7-sonnet') &&
        ((model_options?.max_tokens ?? 0) > 64000 || (model_options?.thinking_budget_tokens ?? 0) > 64000)) {
        requestOptions = { headers: { 'anthropic-beta': 'output-128k-2025-02-19' } };
    }

    const fixedMessages = fixOrphanedToolUse(prompt.messages);
    let sanitizedMessages = sanitizeMessages(fixedMessages);

    if (options.tools) {
        for (const tool of options.tools) {
            if (tool.input_schema.type !== 'object') {
                throw new Error(`Tool "${tool.name}" has invalid input_schema.type: expected "object", got "${tool.input_schema.type}"`);
            }
        }
    }

    const hasTools = options.tools && options.tools.length > 0;
    if (!hasTools && claudeMessagesContainToolBlocks(sanitizedMessages)) {
        sanitizedMessages = convertClaudeToolBlocksToText(sanitizedMessages);
    }

    sanitizedMessages = stripClaudeCacheControlFromMessages(sanitizedMessages);
    const sanitizedSystem = stripClaudeCacheControlFromSystem(prompt.system);
    const sanitizedTools = hasTools
        ? stripClaudeCacheControlFromTools(options.tools as MessageCreateParamsBase['tools'])
        : undefined;

    const cacheEnabled = model_options?.cache_enabled === true;
    if (cacheEnabled) {
        const cacheTtl = model_options?.cache_ttl as '5m' | '1h' | undefined;
        const cacheControl = { type: 'ephemeral' as const, ...(cacheTtl && { ttl: cacheTtl }) };

        if (sanitizedSystem && sanitizedSystem.length > 0) {
            const lastBlock = sanitizedSystem[sanitizedSystem.length - 1] as TextBlockParam & { cache_control?: unknown };
            lastBlock.cache_control = cacheControl;
        }
        if (sanitizedTools && sanitizedTools.length > 0) {
            const lastTool = sanitizedTools[sanitizedTools.length - 1] as ClaudeTool & { cache_control?: unknown };
            lastTool.cache_control = cacheControl;
        }
        if (sanitizedMessages.length >= 4) {
            const pivotMsg = sanitizedMessages[sanitizedMessages.length - 2];
            if (Array.isArray(pivotMsg.content) && pivotMsg.content.length > 0) {
                const lastBlock = pivotMsg.content[pivotMsg.content.length - 1];
                if (typeof lastBlock === 'object' && lastBlock !== null && 'type' in lastBlock &&
                    lastBlock.type !== 'thinking' && lastBlock.type !== 'redacted_thinking') {
                    (lastBlock as TextBlockParam).cache_control = cacheControl;
                }
            }
        }
    }

    const { thinking, outputConfig, hasSamplingRestriction } = resolveClaudeThinking(modelName, model_options as Parameters<typeof resolveClaudeThinking>[1]);

    const payload: MessageCreateParamsBase = {
        messages: sanitizedMessages,
        system: sanitizedSystem,
        tools: sanitizedTools,
        temperature: hasSamplingRestriction ? undefined : model_options?.temperature,
        model: modelName,
        max_tokens: claudeMaxTokens(options),
        top_p: hasSamplingRestriction ? undefined : (model_options?.temperature != null ? undefined : model_options?.top_p),
        top_k: hasSamplingRestriction ? undefined : model_options?.top_k,
        stop_sequences: model_options?.stop_sequence,
        thinking,
        stream: true,
        ...(outputConfig && { output_config: outputConfig }),
    };

    return { payload, requestOptions };
}

// ============================================================================
// Streaming conversation builder (called after stream completes)
// ============================================================================

export function buildClaudeStreamingConversation(
    prompt: ClaudePrompt,
    result: unknown[],
    toolUse: unknown[] | undefined,
    options: ExecutionOptions
): ClaudePrompt {
    const completionResults = result as CompletionResult[];
    const text = completionResults
        .filter((r) => r.type === 'text')
        .map((r) => r.value as string)
        .join('');

    let conversation = updateClaudeConversation(options.conversation as ClaudePrompt | undefined, prompt);

    if (text) {
        const assistantMsg: MessageParam = { role: 'assistant', content: text };
        conversation = updateClaudeConversation(conversation, { messages: [assistantMsg] });
    }

    if (toolUse && toolUse.length > 0) {
        const toolBlocks: ContentBlockParam[] = (toolUse as ToolUse[]).map((t) => ({
            type: 'tool_use' as const,
            id: t.id,
            name: t.tool_name,
            input: t.tool_input ?? {},
        }));
        const assistantToolMsg: MessageParam = { role: 'assistant', content: toolBlocks };
        conversation = updateClaudeConversation(conversation, { messages: [assistantToolMsg] });
    }

    conversation = incrementConversationTurn(conversation) as ClaudePrompt;
    const currentTurn = getConversationMeta(conversation).turnNumber;
    const stripOptions = {
        keepForTurns: options.stripImagesAfterTurns ?? Infinity,
        currentTurn,
        textMaxTokens: options.stripTextMaxTokens,
    };
    let processed = stripBase64ImagesFromConversation(conversation, stripOptions);
    processed = truncateLargeTextInConversation(processed, stripOptions);
    processed = stripHeartbeatsFromConversation(processed, {
        keepForTurns: options.stripHeartbeatsAfterTurns ?? 1,
        currentTurn,
    });
    return processed as ClaudePrompt;
}

// ============================================================================
// Execution helpers (standalone, take a client parameter)
// ============================================================================

/**
 * Execute a non-streaming Claude completion.
 * Works with any Anthropic-compatible client (Anthropic or AnthropicVertex).
 */
export async function executeClaudeCompletion(
    client: Anthropic | AnthropicVertex,
    prompt: ClaudePrompt,
    options: ExecutionOptions,
): Promise<Completion> {
    const model_options = options.model_options as ClaudeBaseOptions | undefined;

    let conversation = updateClaudeConversation(options.conversation as ClaudePrompt | undefined, prompt);

    const { payload, requestOptions } = getClaudePayload(options, conversation);

    const result: Message = await client.messages.stream(payload, requestOptions).finalMessage();

    const includeThoughts = model_options?.include_thoughts ?? false;
    const text = collectAllTextContent(result.content, includeThoughts);
    const tool_use = collectClaudeTools(result.content);

    conversation = updateClaudeConversation(conversation, createPromptFromResponse(result));
    conversation = incrementConversationTurn(conversation) as ClaudePrompt;
    const currentTurn = getConversationMeta(conversation).turnNumber;
    const stripOpts = {
        keepForTurns: options.stripImagesAfterTurns ?? Infinity,
        currentTurn,
        textMaxTokens: options.stripTextMaxTokens,
    };
    let processedConversation = stripBase64ImagesFromConversation(conversation, stripOpts);
    processedConversation = truncateLargeTextInConversation(processedConversation, stripOpts);
    processedConversation = stripHeartbeatsFromConversation(processedConversation, {
        keepForTurns: options.stripHeartbeatsAfterTurns ?? 1,
        currentTurn,
    });

    return {
        result: text ? [{ type: 'text', value: text }] : [{ type: 'text', value: '' }],
        tool_use,
        token_usage: anthropicUsageToTokenUsage(result.usage),
        finish_reason: tool_use ? 'tool_use' : claudeFinishReason(result?.stop_reason ?? ''),
        conversation: processedConversation,
    };
}

/**
 * Execute a streaming Claude completion.
 * Works with any Anthropic-compatible client (Anthropic or AnthropicVertex).
 */
export async function streamClaudeCompletion(
    client: Anthropic | AnthropicVertex,
    prompt: ClaudePrompt,
    options: ExecutionOptions,
): Promise<AsyncIterable<CompletionChunkObject>> {
    const model_options = options.model_options as ClaudeBaseOptions | undefined;
    const conversation = updateClaudeConversation(options.conversation as ClaudePrompt | undefined, prompt);

    const { payload, requestOptions } = getClaudePayload(options, conversation);
    const streamingPayload: MessageStreamParams = { ...payload, stream: true };

    const response_stream = await client.messages.stream(streamingPayload, requestOptions);

    let currentToolUse: { id: string; name: string; inputJson: string } | null = null;
    let pendingSpacing = false;

    const stream = asyncMap(response_stream, async (streamEvent: RawMessageStreamEvent) => {
        switch (streamEvent.type) {
            case 'message_start':
                return {
                    result: [{ type: 'text', value: '' }],
                    token_usage: anthropicUsageToTokenUsage(streamEvent.message.usage as AnthropicUsageLike),
                } satisfies CompletionChunkObject;
            case 'message_delta':
                return {
                    result: [{ type: 'text', value: '' }],
                    token_usage: { result: streamEvent.usage.output_tokens },
                    finish_reason: claudeFinishReason(streamEvent.delta.stop_reason ?? undefined),
                } satisfies CompletionChunkObject;
            case 'content_block_start':
                if (streamEvent.content_block.type === 'tool_use') {
                    currentToolUse = { id: streamEvent.content_block.id, name: streamEvent.content_block.name, inputJson: '' };
                    return {
                        result: [],
                        tool_use: [{
                            id: streamEvent.content_block.id,
                            tool_name: streamEvent.content_block.name,
                            tool_input: '' as unknown as JSONObject,
                        }],
                    } satisfies CompletionChunkObject;
                }
                if (streamEvent.content_block.type === 'redacted_thinking' && model_options?.include_thoughts) {
                    return {
                        result: [{ type: 'text', value: `[Redacted thinking: ${streamEvent.content_block.data}]` }],
                    } satisfies CompletionChunkObject;
                }
                break;
            case 'content_block_delta':
                switch (streamEvent.delta.type) {
                    case 'text_delta': {
                        const prefix = pendingSpacing ? '\n\n' : '';
                        pendingSpacing = false;
                        return {
                            result: streamEvent.delta.text ? [{ type: 'text', value: prefix + streamEvent.delta.text }] : [],
                        } satisfies CompletionChunkObject;
                    }
                    case 'input_json_delta':
                        if (currentToolUse && streamEvent.delta.partial_json) {
                            return {
                                result: [],
                                tool_use: [{
                                    id: currentToolUse.id,
                                    tool_name: '',
                                    tool_input: streamEvent.delta.partial_json as unknown as JSONObject,
                                }],
                            } satisfies CompletionChunkObject;
                        }
                        break;
                    case 'thinking_delta':
                        if (model_options?.include_thoughts) {
                            return {
                                result: streamEvent.delta.thinking ? [{ type: 'text', value: streamEvent.delta.thinking }] : [],
                            } satisfies CompletionChunkObject;
                        }
                        break;
                    case 'signature_delta':
                        if (model_options?.include_thoughts) {
                            pendingSpacing = true;
                        }
                        break;
                }
                break;
            case 'content_block_stop':
                if (currentToolUse) {
                    currentToolUse = null;
                    pendingSpacing = false;
                }
                break;
        }

        return { result: [] } satisfies CompletionChunkObject;
    });

    return stream;
}

// ============================================================================
// Error handling
// ============================================================================

export function formatAnthropicLlumiverseError(error: unknown, context: LlumiverseErrorContext): LlumiverseError {
    if (error instanceof AnthropicError && !(error instanceof APIError)) {
        // Client-side SDK error (e.g. "Streaming is required for operations that may take longer than 10 minutes").
        // These are structural/configuration errors — retrying will never succeed.
        const errorName = error.constructor?.name || 'AnthropicError';
        return new LlumiverseError(`[${context.provider}] ${error.message}`, false, context, error, undefined, errorName);
    }
    if (!(error instanceof APIError)) {
        // Not an Anthropic error — rethrow for default handling
        throw error;
    }

    const apiError = error as APIError;
    const httpStatusCode = apiError.status;
    let message = apiError.message || String(error);
    let errorType: string | undefined;

    if (apiError.error && typeof apiError.error === 'object') {
        const nested = apiError.error as Record<string, unknown>;
        if (nested['error'] && typeof nested['error'] === 'object') {
            const innerError = nested['error'] as Record<string, unknown>;
            errorType = innerError['type'] as string | undefined;
            if (typeof innerError['message'] === 'string') {
                message = innerError['message'];
            }
        }
    }

    let userMessage = message;
    if (httpStatusCode) userMessage = `[${httpStatusCode}] ${userMessage}`;
    if (errorType && errorType !== 'error') userMessage = `${errorType}: ${userMessage}`;
    if (apiError.requestID) userMessage += ` (Request ID: ${apiError.requestID})`;

    const retryable = isClaudeErrorRetryable(error, httpStatusCode, errorType, apiError.headers ?? undefined);
    const errorName = error.constructor?.name || 'AnthropicError';

    return new LlumiverseError(`[${context.provider}] ${userMessage}`, retryable, context, error, httpStatusCode, errorName);
}

export function isClaudeErrorRetryable(
    error: unknown,
    httpStatusCode: number | undefined,
    errorType: string | undefined,
    headers?: Headers | undefined,
): boolean | undefined {
    // Honour the server's explicit retry directive first (mirrors SDK shouldRetry logic).
    const shouldRetryHeader = headers?.get('x-should-retry');
    if (shouldRetryHeader === 'true') return true;
    if (shouldRetryHeader === 'false') return false;

    if (error instanceof APIUserAbortError) return false;
    if (error instanceof RateLimitError) return true;
    if (error instanceof InternalServerError) return true;
    if (error instanceof APIConnectionTimeoutError) return true;
    if (error instanceof BadRequestError) return false;
    if (error instanceof AuthenticationError) return false;
    if (error instanceof PermissionDeniedError) return false;
    if (error instanceof NotFoundError) return false;
    if (error instanceof ConflictError) return true;  // SDK retries 409 (lock timeouts)
    if (error instanceof UnprocessableEntityError) return false;
    if (errorType === 'invalid_request_error') return false;
    if (httpStatusCode !== undefined) {
        if (httpStatusCode === 429 || httpStatusCode === 408 || httpStatusCode === 529) return true;
        if (httpStatusCode >= 500 && httpStatusCode < 600) return true;
        if (httpStatusCode >= 400 && httpStatusCode < 500) return false;
    }
    if (error instanceof APIConnectionError && !(error instanceof APIConnectionTimeoutError)) return true;
    return undefined;
}
