/**
 * Custom streamSimple for the local Shortcut provider.
 *
 * Thin composition: converts context → request body, fetches the local
 * `/api/llm/stream` SSE proxy, maps SSE events → owned AI AssistantMessageEvents.
 *
 * This is the desktop/local provider path. Hosted `--cloud` mode does not use
 * this stream; cloud mode runs model calls inside the hosted WebSocket runtime.
 *
 * Message conversion lives in message-converter.ts.
 * SSE protocol types live in sse-protocol.ts.
 */
import { createAssistantMessageEventStream } from '../../ai/utils/event-stream.js';
import { APP_NAME } from '../../config.js';
import { SHORTCUT_LLM_PROXY_ENDPOINTS } from '../../endpoints.js';
import { resolveShortcutSpeedForContext } from '../../fast-mode.js';
import { SHORTCUT_PROVIDER_ID, isShortcutFastModeModel } from '../../model-ids.js';
import { buildUsage, mapStopReason } from './llm-usage.js';
import { convertMessages, convertTools } from './message-converter.js';
import { readShortcutSSE } from './shortcut-sse.js';
import { THINKING_TO_EFFORT } from './sse-protocol.js';
/**
 * Create a streaming function for the local Shortcut LLM proxy.
 * Returns a owned AI StreamFn that connects to `/api/llm/stream` (SSE) and
 * translates SSE events into owned AI AssistantMessageEvents in real time.
 *
 * Do not use this in hosted cloud mode. The hosted runtime should call direct
 * cloud model providers and stream over the WebSocket session protocol instead.
 *
 * @param onContentBlock — optional callback for `content_block` events (e.g., citation data)
 */
export function createShortcutStream(baseUrl, getExtraHeaders, onContentBlock, recoverAuth) {
    return function shortcutStream(model, context, options) {
        const stream = createAssistantMessageEventStream();
        const shortcutOptions = options;
        const partial = {
            role: 'assistant',
            content: [],
            api: 'shortcut-llm',
            provider: SHORTCUT_PROVIDER_ID,
            model: model.id,
            usage: {
                input: 0,
                output: 0,
                cacheRead: 0,
                cacheWrite: 0,
                totalTokens: 0,
                cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
            },
            stopReason: 'stop',
            timestamp: Date.now()
        };
        const speed = shortcutOptions?.speed === 'standard'
            ? 'standard'
            : shortcutOptions?.speed === 'fast' && isShortcutFastModeModel(model)
                ? resolveShortcutSpeedForContext(model.id, shortcutOptions.speed, context)
                : undefined;
        const body = {
            model: model.id,
            messages: convertMessages(context),
            tools: convertTools(context.tools),
            effort_level: options?.reasoning
                ? (THINKING_TO_EFFORT[options.reasoning] ?? 'low')
                : undefined,
            thinking_type: options?.reasoning ? 'adaptive' : 'off',
            ...(speed !== undefined ? { speed } : {}),
            ...(shortcutOptions?.userMessageId ? { user_message_id: shortcutOptions.userMessageId } : {}),
            max_tokens: options?.maxTokens
        };
        const thinkingText = {};
        const toolCallArgs = {};
        (async () => {
            try {
                const requestStream = async (apiKey) => await fetch(`${baseUrl}${SHORTCUT_LLM_PROXY_ENDPOINTS.stream}`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        ...getExtraHeaders?.(),
                        Authorization: `Bearer ${apiKey}`,
                        'User-AgentController': `Mozilla/5.0 (compatible; ${APP_NAME}-coding-agent/1.0)`
                    },
                    body: JSON.stringify(body),
                    signal: options?.signal
                });
                let apiKey = options?.apiKey ?? '';
                let response = await requestStream(apiKey);
                if (response.status === 401 && recoverAuth) {
                    try {
                        const recoveredApiKey = await recoverAuth(apiKey);
                        if (recoveredApiKey) {
                            apiKey = recoveredApiKey;
                            response = await requestStream(apiKey);
                        }
                    }
                    catch {
                        // Keep the original 401 response path below.
                    }
                }
                if (!response.ok) {
                    const raw = await response.text();
                    const text = raw.split('\n').slice(0, 20).join('\n');
                    if (response.status === 401) {
                        throw new Error(`Session expired. Run /login to re-authenticate. Raw error: LLM proxy error 401: ${text}`);
                    }
                    throw new Error(`LLM proxy error ${response.status}: ${text}`);
                }
                stream.push({ type: 'start', partial });
                await readShortcutSSE(response, async (event, payload) => {
                    handleSSEEvent(event, payload, partial, stream, thinkingText, toolCallArgs, model, onContentBlock);
                });
            }
            catch (err) {
                const isAbort = options?.signal?.aborted || (err instanceof Error && err.name === 'AbortError');
                const errMsg = err instanceof Error ? err.message : 'Request failed';
                partial.stopReason = isAbort ? 'aborted' : 'error';
                partial.errorMessage = isAbort ? 'Request was aborted' : errMsg;
                stream.push({ type: 'error', reason: isAbort ? 'aborted' : 'error', error: partial });
            }
        })();
        return stream;
    };
}
/** Pad partial.content with empty text placeholders up to the given index. */
function padContent(partial, ci) {
    while (partial.content.length <= ci) {
        partial.content.push({ type: 'text', text: '' });
    }
}
/** Map a single SSE event to owned AI stream events and update partial state. */
function handleSSEEvent(event, payload, partial, stream, thinkingText, toolCallArgs, model, onContentBlock) {
    switch (event) {
        case 'thinking_start': {
            const p = payload;
            const ci = p.content_index;
            thinkingText[ci] = '';
            padContent(partial, ci);
            partial.content[ci] = {
                type: 'thinking',
                thinking: '',
                redacted: false,
                thinkingSignature: ''
            };
            stream.push({ type: 'thinking_start', contentIndex: ci, partial });
            break;
        }
        case 'thinking_delta': {
            const p = payload;
            const ci = p.content_index;
            thinkingText[ci] = (thinkingText[ci] || '') + p.delta;
            const tc = partial.content[ci];
            if (tc)
                tc.thinking = thinkingText[ci];
            stream.push({ type: 'thinking_delta', contentIndex: ci, delta: p.delta, partial });
            break;
        }
        case 'thinking_end': {
            const p = payload;
            const ci = p.content_index;
            const tc = partial.content[ci];
            if (tc)
                tc.thinkingSignature = p.signature;
            stream.push({
                type: 'thinking_end',
                contentIndex: ci,
                content: thinkingText[ci] || '',
                partial
            });
            break;
        }
        case 'thinking_redacted': {
            const p = payload;
            const ci = p.content_index;
            padContent(partial, ci);
            partial.content[ci] = {
                type: 'thinking',
                thinking: '',
                redacted: true,
                thinkingSignature: p.signature
            };
            stream.push({ type: 'thinking_start', contentIndex: ci, partial });
            stream.push({ type: 'thinking_end', contentIndex: ci, content: '', partial });
            break;
        }
        case 'text_start': {
            const p = payload;
            const ci = p.content_index;
            padContent(partial, ci);
            partial.content[ci] = { type: 'text', text: '' };
            stream.push({ type: 'text_start', contentIndex: ci, partial });
            break;
        }
        case 'text_delta': {
            const p = payload;
            const ci = p.content_index;
            const tc = partial.content[ci];
            if (tc)
                tc.text += p.delta;
            stream.push({ type: 'text_delta', contentIndex: ci, delta: p.delta, partial });
            break;
        }
        case 'text_end': {
            const ci = payload.content_index;
            const tc = partial.content[ci];
            stream.push({
                type: 'text_end',
                contentIndex: ci,
                content: tc?.text || '',
                partial
            });
            break;
        }
        case 'toolcall_start': {
            const p = payload;
            const ci = p.content_index;
            toolCallArgs[ci] = '';
            padContent(partial, ci);
            partial.content[ci] = {
                type: 'toolCall',
                id: p.id,
                name: p.name,
                arguments: {}
            };
            stream.push({ type: 'toolcall_start', contentIndex: ci, partial });
            break;
        }
        case 'toolcall_delta': {
            const p = payload;
            const ci = p.content_index;
            toolCallArgs[ci] = (toolCallArgs[ci] || '') + p.delta;
            stream.push({ type: 'toolcall_delta', contentIndex: ci, delta: p.delta, partial });
            break;
        }
        case 'toolcall_end': {
            const p = payload;
            const ci = p.content_index;
            let args = {};
            try {
                args = JSON.parse(p.arguments);
            }
            catch {
                // keep empty
            }
            const toolCall = {
                type: 'toolCall',
                id: p.id,
                name: p.name,
                arguments: args
            };
            partial.content[ci] = toolCall;
            stream.push({ type: 'toolcall_end', contentIndex: ci, toolCall, partial });
            break;
        }
        case 'content_block': {
            // Forward full content blocks (e.g., text blocks with citation arrays)
            // to the handler for citation extraction. These are emitted by the proxy
            // alongside text_delta events when Anthropic returns citation metadata.
            if (onContentBlock) {
                onContentBlock(payload);
            }
            break;
        }
        case 'done': {
            const p = payload;
            partial.usage = buildUsage(p.usage, model);
            const reason = mapStopReason(p.stop_reason);
            partial.stopReason = reason;
            stream.push({
                type: 'done',
                reason: reason,
                message: partial
            });
            break;
        }
        case 'error': {
            const p = payload;
            throw new Error(p.message);
        }
    }
}
//# sourceMappingURL=shortcut-stream.js.map