import { isAbortError, resolveBaseUrl, sanitizeModelSettings } from './helpers';
import { buildOllamaMessages } from './messages';
import { createOllamaToolContext } from './toolContext';
import { applyApprovedToolCalls, applyRejectedToolCalls, buildAssistantToolMessage, executeToolCall, runAttachmentToolCall } from './toolExecution';
import { processBufferedLines } from './streaming';
import { OllamaRunState } from './runState';
const runOllamaWorkflow = async (params) => {
    const { agent, baseUrl, headers, conversation, callbacks, signal, existingState, attachments } = params;
    const toolContext = await createOllamaToolContext(agent, { attachments });
    const cleanup = async () => {
        await toolContext.dispose();
    };
    try {
        if (existingState) {
            const { approved, rejected } = existingState.consumeResolvedCalls();
            await applyRejectedToolCalls(conversation, rejected, callbacks);
            await applyApprovedToolCalls(conversation, approved, toolContext, callbacks);
        }
        let finalOutput = '';
        const aggregatedEvents = [];
        while (true) {
            const payload = {
                model: agent.options.modelName,
                stream: true,
                messages: conversation,
                options: sanitizeModelSettings(agent.options.modelSettings),
                tools: toolContext.definitions.map((definition) => ({
                    type: 'function',
                    function: {
                        name: definition.name,
                        description: definition.description,
                        parameters: definition.parameters ?? { type: 'object' }
                    }
                }))
            };
            const endpoint = `${baseUrl}/api/chat`;
            const response = await fetch(endpoint, {
                method: 'POST',
                headers,
                body: JSON.stringify(payload),
                signal
            });
            if (!response.ok) {
                let errorBody = '';
                try {
                    errorBody = await response.text();
                }
                catch {
                    // ignore body read errors
                }
                let detailedMessage = '';
                if (errorBody) {
                    try {
                        const parsed = JSON.parse(errorBody);
                        const parsedMessage = typeof parsed?.error === 'string'
                            ? parsed.error
                            : typeof parsed?.message === 'string'
                                ? parsed.message
                                : null;
                        detailedMessage = parsedMessage ?? errorBody;
                    }
                    catch {
                        detailedMessage = errorBody;
                    }
                }
                const statusLabel = response.statusText?.trim().length
                    ? `${response.status} ${response.statusText}`
                    : `${response.status}`;
                const baseMessage = `Ollama request failed with status ${statusLabel}`;
                throw new Error(detailedMessage ? `${baseMessage}: ${detailedMessage}` : baseMessage);
            }
            const reader = response.body?.getReader();
            if (!reader) {
                throw new Error('Ollama response did not include a readable body stream');
            }
            const decoder = new TextDecoder();
            let buffer = '';
            const state = {
                finalOutput: '',
                streamFinished: false,
                messageCreated: false,
                rawEvents: []
            };
            const toolCollector = new Map();
            try {
                while (true) {
                    const { value, done } = await reader.read();
                    if (done) {
                        break;
                    }
                    buffer += decoder.decode(value, { stream: true });
                    buffer = await processBufferedLines(buffer, state, callbacks, toolCollector);
                    if (state.streamFinished) {
                        break;
                    }
                }
                const trailing = buffer + decoder.decode();
                const trailingTrimmed = trailing.trim();
                if (trailingTrimmed.length > 0) {
                    console.debug('[Ollama] trailing stream content detected', {
                        snippet: trailingTrimmed.slice(0, 100),
                        length: trailingTrimmed.length,
                        streamFinished: state.streamFinished
                    });
                }
                if (!state.streamFinished && trailingTrimmed.length > 0) {
                    await processBufferedLines(trailingTrimmed, state, callbacks, toolCollector);
                }
                if (!state.messageCreated && typeof state.finalOutput === 'string' && state.finalOutput.length === 0) {
                    state.finalOutput = trailingTrimmed;
                }
                if (!state.messageCreated && typeof state.finalOutput === 'string' && state.finalOutput.length > 0) {
                    console.debug('[Ollama] emitting synthetic onMessageOutputCreated event', {
                        length: state.finalOutput.length
                    });
                    if (callbacks.onMessageOutputCreated) {
                        await callbacks.onMessageOutputCreated(state.finalOutput);
                    }
                    state.messageCreated = true;
                }
                finalOutput = state.finalOutput;
                aggregatedEvents.push(...state.rawEvents);
                const toolCalls = Array.from(toolCollector.values());
                if (toolCalls.length === 0) {
                    if (callbacks.onFinalOutputAccessed) {
                        await callbacks.onFinalOutputAccessed(finalOutput);
                    }
                    console.debug('[Ollama] run completed without tool calls', {
                        outputLength: finalOutput.length
                    });
                    return {
                        finalOutput,
                        rawResponse: aggregatedEvents
                    };
                }
                const assistantMessage = buildAssistantToolMessage(finalOutput, toolCalls);
                conversation.push(assistantMessage);
                const askFirstCalls = [];
                const autoExecuteCalls = [];
                for (const call of toolCalls) {
                    const tool = toolContext.toolsByName.get(call.toolName);
                    if (!tool) {
                        console.warn(`Received tool call for unknown tool "${call.toolName}"`);
                        continue;
                    }
                    console.debug('[Ollama] tool call received', {
                        callId: call.callId,
                        toolName: tool.originalName,
                        permission: tool.permission
                    });
                    if (tool.permission === 'ask_first') {
                        askFirstCalls.push({
                            callId: call.callId,
                            toolUniqueName: tool.uniqueName,
                            toolDisplayName: tool.originalName,
                            serverId: tool.serverId,
                            serverName: tool.serverName,
                            arguments: call.arguments,
                            status: 'pending'
                        });
                        await callbacks.onToolCall?.(tool.originalName, call.arguments, {
                            callId: call.callId,
                            status: 'pending',
                            timestamp: Date.now()
                        });
                    }
                    else if (tool.permission === 'enabled') {
                        if (tool.kind === 'mcp' && (!call.arguments || call.arguments.trim().length === 0)) {
                            const reason = 'Tool call missing arguments.';
                            const timestamp = Date.now();
                            console.warn('[Ollama] Skipping tool execution due to missing arguments', {
                                callId: call.callId,
                                toolName: tool.originalName
                            });
                            await callbacks.onToolCall?.(tool.originalName, call.arguments, {
                                callId: call.callId,
                                status: 'failed',
                                error: reason,
                                timestamp
                            });
                            await callbacks.onToolOutput?.(tool.originalName, reason, {
                                callId: call.callId,
                                status: 'failed',
                                error: reason,
                                timestamp
                            });
                            conversation.push({
                                role: 'tool',
                                content: JSON.stringify({ error: reason }),
                                name: tool.uniqueName,
                                tool_call_id: call.callId
                            });
                            continue;
                        }
                        autoExecuteCalls.push(call);
                    }
                    else {
                        console.warn(`Tool "${tool.originalName}" is disabled; generating rejection message.`);
                        const reason = 'Tool disabled by configuration.';
                        const timestamp = Date.now();
                        await callbacks.onToolCall?.(tool.originalName, call.arguments, {
                            callId: call.callId,
                            status: 'failed',
                            error: reason,
                            timestamp
                        });
                        await callbacks.onToolOutput?.(tool.originalName, reason, {
                            callId: call.callId,
                            status: 'failed',
                            error: reason,
                            timestamp
                        });
                        conversation.push({
                            role: 'tool',
                            content: JSON.stringify({ error: reason }),
                            name: tool.uniqueName,
                            tool_call_id: call.callId
                        });
                    }
                }
                if (askFirstCalls.length > 0) {
                    const runState = new OllamaRunState({
                        conversation,
                        calls: askFirstCalls
                    });
                    return {
                        finalOutput,
                        interruptions: runState.getInterruptions(),
                        state: runState,
                        rawResponse: aggregatedEvents
                    };
                }
                for (const call of autoExecuteCalls) {
                    const tool = toolContext.toolsByName.get(call.toolName);
                    if (!tool) {
                        continue;
                    }
                    const result = tool.kind === 'attachment'
                        ? await runAttachmentToolCall(call, tool, callbacks)
                        : await executeToolCall(call, tool, callbacks);
                    conversation.push({
                        role: 'tool',
                        content: result.output,
                        name: tool.uniqueName,
                        tool_call_id: call.callId
                    });
                }
                continue;
            }
            catch (error) {
                if (isAbortError(error)) {
                    throw error;
                }
                throw new Error(error instanceof Error ? error.message : String(error));
            }
            finally {
                try {
                    reader.releaseLock();
                }
                catch {
                    // ignore release errors
                }
            }
        }
    }
    finally {
        cleanup().catch((error) => {
            console.warn('Failed to dispose Ollama MCP tool context', error);
        });
    }
};
const runOllamaChat = async (agent, input, callbacks, signal) => {
    const baseUrl = resolveBaseUrl(agent);
    const headers = createHeaders(agent);
    const conversation = buildOllamaMessages(agent, input);
    return runOllamaWorkflow({
        agent,
        baseUrl,
        headers,
        conversation,
        callbacks,
        signal,
        attachments: input.attachments
    });
};
const createHeaders = (agent) => {
    const headers = {
        'Content-Type': 'application/json'
    };
    if (agent.options.clientHeaders) {
        for (const [key, value] of Object.entries(agent.options.clientHeaders)) {
            headers[key] = value;
        }
    }
    return headers;
};
const assertOllamaRunState = (state) => {
    if (!state) {
        throw new Error('Ollama resume run requires a run state');
    }
    if (state instanceof OllamaRunState) {
        return state;
    }
    if (typeof state.toString === 'function') {
        try {
            return OllamaRunState.deserialize(state.toString());
        }
        catch (error) {
            const detail = error instanceof Error ? error.message : String(error);
            throw new Error(`Ollama resume run received incompatible state: ${detail}`);
        }
    }
    throw new Error('Ollama resume run received incompatible state instance');
};
const resumeOllamaChat = async (agent, input, callbacks, signal) => {
    const baseUrl = resolveBaseUrl(agent);
    const headers = createHeaders(agent);
    const state = assertOllamaRunState(input.state);
    const conversation = state.cloneConversation();
    return runOllamaWorkflow({
        agent,
        baseUrl,
        headers,
        conversation,
        callbacks,
        signal,
        existingState: state,
        attachments: input.attachments
    });
};
export const startOllamaChatRun = async (agent, input, callbacks) => {
    const abortController = input.abortController ?? new AbortController();
    const signal = input.signal ?? abortController.signal;
    const runPromise = runOllamaChat(agent, input, callbacks, signal);
    return {
        cancel: async () => {
            if (!signal.aborted) {
                abortController.abort();
            }
            try {
                await runPromise;
            }
            catch (error) {
                if (!isAbortError(error)) {
                    throw error;
                }
            }
        },
        completed: runPromise,
        signal
    };
};
export const resumeOllamaChatRun = async (agent, input, callbacks) => {
    const abortController = input.abortController ?? new AbortController();
    const signal = input.signal ?? abortController.signal;
    const runPromise = resumeOllamaChat(agent, input, callbacks, signal);
    return {
        cancel: async () => {
            if (!signal.aborted) {
                abortController.abort();
            }
            try {
                await runPromise;
            }
            catch (error) {
                if (!isAbortError(error)) {
                    throw error;
                }
            }
        },
        completed: runPromise,
        signal
    };
};
export { runOllamaWorkflow };
