import { BaseAIService } from '../base_ai_service.js';
import type { Message, ChatCompletionOptions, ChatResponse, StreamChunk } from '../ai_interface.js';
import { OllamaMessageFormatter } from '../formatters/ollama_formatter.js';
import log from '../../log.js';
import type { ToolCall, Tool } from '../tools/tool_interfaces.js';
import toolRegistry from '../tools/tool_registry.js';
import type { OllamaOptions } from './provider_options.js';
import { getOllamaOptions } from './providers.js';
import { Ollama, type ChatRequest } from 'ollama';
import options from '../../options.js';
import {
    StreamProcessor,
    createStreamHandler,
    performProviderHealthCheck,
    processProviderStream,
    extractStreamStats
} from './stream_handler.js';

// Add an interface for tool execution feedback status
interface ToolExecutionStatus {
    toolCallId: string;
    name: string;
    success: boolean;
    result: string;
    error?: string;
}

// Interface for Ollama-specific messages
interface OllamaMessage {
    role: string;
    content: string;
    tool_call_id?: string;
    tool_calls?: OllamaToolCall[];
    name?: string;
}

// Interface for Ollama tool calls
interface OllamaToolCall {
    id: string;
    function: {
        name: string;
        arguments: Record<string, unknown>;
    };
}

// Interface for Ollama request options
interface OllamaRequestOptions {
    model: string;
    messages: OllamaMessage[];
    stream?: boolean;
    options?: Record<string, unknown>;
    format?: string;
    tools?: Tool[];
    [key: string]: unknown;
}

export class OllamaService extends BaseAIService {
    private formatter: OllamaMessageFormatter;
    private client: Ollama | null = null;

    constructor() {
        super('Ollama');
        this.formatter = new OllamaMessageFormatter();
    }

    override isAvailable(): boolean {
        return super.isAvailable() && !!options.getOption('ollamaBaseUrl');
    }

    private getClient(): Ollama {
        if (!this.client) {
            const baseUrl = options.getOption('ollamaBaseUrl');
            if (!baseUrl) {
                throw new Error('Ollama base URL is not configured');
            }

            log.info(`Creating new Ollama client with base URL: ${baseUrl}`);

            // Create client with debug options
            try {
                this.client = new Ollama({
                    host: baseUrl,
                    fetch: (url, init) => {
                        log.info(`Ollama API request to: ${url}`);
                        log.info(`Ollama API request method: ${init?.method || 'GET'}`);
                        log.info(`Ollama API request headers: ${JSON.stringify(init?.headers || {})}`);

                        // Call the actual fetch
                        return fetch(url, init).then(response => {
                            log.info(`Ollama API response status: ${response.status}`);
                            if (!response.ok) {
                                log.error(`Ollama API error response: ${response.statusText}`);
                            }
                            return response;
                        }).catch(error => {
                            log.error(`Ollama API fetch error: ${error.message}`);
                            throw error;
                        });
                    }
                });

                log.info(`Ollama client successfully created`);
            } catch (error) {
                log.error(`Error creating Ollama client: ${error}`);
                throw error;
            }
        }
        return this.client;
    }

    async generateChatCompletion(messages: Message[], opts: ChatCompletionOptions = {}): Promise<ChatResponse> {
        if (!this.isAvailable()) {
            throw new Error('Ollama service is not available. Check API URL in settings.');
        }

        // Get provider-specific options from the central provider manager
        const providerOptions = await getOllamaOptions(opts);

        // Log provider metadata if available
        if (providerOptions.providerMetadata) {
            log.info(`Using model ${providerOptions.model} from provider ${providerOptions.providerMetadata.provider}`);

            // Log capabilities if available
            const capabilities = providerOptions.providerMetadata.capabilities;
            if (capabilities) {
                log.info(`Model capabilities: ${JSON.stringify(capabilities)}`);
            }
        }

        const systemPrompt = this.getSystemPrompt(providerOptions.systemPrompt || options.getOption('aiSystemPrompt'));

        try {
            // Check if we should add tool execution feedback
            if (providerOptions.toolExecutionStatus && Array.isArray(providerOptions.toolExecutionStatus) && providerOptions.toolExecutionStatus.length > 0) {
                log.info(`Adding tool execution feedback to messages`);
                messages = this.addToolExecutionFeedback(messages, providerOptions.toolExecutionStatus as ToolExecutionStatus[]);
            }

            // Determine whether to use the formatter or send messages directly
            let messagesToSend: Message[];

            if (providerOptions.bypassFormatter) {
                // Bypass the formatter entirely - use messages as is
                messagesToSend = [...messages];
                log.info(`Bypassing formatter for Ollama request with ${messages.length} messages`);
            } else {
                // Determine if tools will be used in this request
                const willUseTools = providerOptions.enableTools !== false;
                
                // Use the formatter to prepare messages
                messagesToSend = this.formatter.formatMessages(
                    messages,
                    systemPrompt,
                    undefined, // context
                    providerOptions.preserveSystemPrompt,
                    willUseTools // Pass flag indicating if tools will be used
                );
                
                log.info(`Sending to Ollama with formatted messages: ${messagesToSend.length}${willUseTools ? ' (with tool instructions)' : ''}`);
            }

            // Get tools if enabled
            let tools: Tool[] = [];
            if (providerOptions.enableTools !== false) {
                try {
                    tools = providerOptions.tools && providerOptions.tools.length > 0
                        ? providerOptions.tools as Tool[]
                        : toolRegistry.getAllToolDefinitions();

                    // Handle empty tools array
                    if (tools.length === 0) {
                        log.info('No tools found, attempting to initialize tools...');
                        // Tools are already initialized in the AIServiceManager constructor
                        // No need to initialize them again
                        tools = toolRegistry.getAllToolDefinitions();
                        log.info(`After initialization: ${tools.length} tools available`);
                    }

                    if (tools.length > 0) {
                        log.info(`Sending ${tools.length} tool definitions to Ollama`);
                    }
                } catch (error) {
                    const errorMsg = error instanceof Error ? error.message : String(error);
                    log.error(`Error preparing tools: ${errorMsg}`);
                    tools = []; // Empty fallback
                }
            }

            // Convert our message format to Ollama's format
            const convertedMessages = messagesToSend.map(msg => {
                const converted: OllamaMessage = {
                    role: msg.role,
                    content: msg.content
                };

                if (msg.tool_calls) {
                    converted.tool_calls = msg.tool_calls.map(tc => {
                        // For Ollama, arguments must be an object, not a string
                        let processedArgs: Record<string, unknown> = {};

                        // If arguments is a string, try to parse it as JSON
                        if (typeof tc.function.arguments === 'string') {
                            try {
                                processedArgs = JSON.parse(tc.function.arguments);
                            } catch (e) {
                                // If parsing fails, create an object with a single property
                                log.info(`Could not parse tool arguments as JSON: ${e}`);
                                processedArgs = { raw: tc.function.arguments };
                            }
                        } else if (typeof tc.function.arguments === 'object') {
                            processedArgs = tc.function.arguments as Record<string, unknown>;
                        }

                        return {
                            id: tc.id ?? '',
                            function: {
                                name: tc.function.name,
                                arguments: processedArgs
                            }
                        };
                    });
                }

                if (msg.tool_call_id) {
                    converted.tool_call_id = msg.tool_call_id;
                }

                if (msg.name) {
                    converted.name = msg.name;
                }

                return converted;
            });

            // Prepare base request options
            const baseRequestOptions: OllamaRequestOptions = {
                model: providerOptions.model,
                messages: convertedMessages,
                stream: opts.stream === true
            };

            // Add tool definitions if available
            if (tools && tools.length > 0 && providerOptions.enableTools !== false) {
                baseRequestOptions.tools = tools;
            }

            // Add any model-specific parameters
            if (providerOptions.options) {
                baseRequestOptions.options = providerOptions.options;
            }

            // If JSON response is expected, set format
            if (providerOptions.expectsJsonResponse) {
                baseRequestOptions.format = 'json';
            }

            log.info(`Sending request to Ollama with model: ${providerOptions.model}`);

            // Handle streaming vs non-streaming responses
            const client = this.getClient();

            if (opts.stream === true) {
                // Use streaming API
                return this.handleStreamingResponse(client, baseRequestOptions, opts, providerOptions);
            } else {
                // Use non-streaming API
                try {
                    log.info(`Sending non-streaming request to Ollama`);
                    // Create a properly typed request with stream: false
                    const chatRequest: ChatRequest & { stream?: false } = {
                        ...baseRequestOptions,
                        stream: false
                    };

                    const response = await client.chat(chatRequest);

                    log.info(`Received response from Ollama`);

                    // Transform tool calls if present
                    const toolCalls = this.transformToolCalls(response.message.tool_calls);

                    return {
                        text: response.message.content,
                        model: providerOptions.model,
                        provider: 'ollama',
                        tool_calls: toolCalls.length > 0 ? toolCalls : undefined
                    };
                } catch (error) {
                    const errorMsg = error instanceof Error ? error.message : String(error);
                    log.error(`Error in Ollama request: ${errorMsg}`);
                    throw error;
                }
            }
        } catch (error) {
            const errorMsg = error instanceof Error ? error.message : String(error);
            log.error(`Error in Ollama service: ${errorMsg}`);
            throw error;
        }
    }

    /**
     * Handle streaming response from Ollama
     *
     * Uses reusable stream handling utilities for processing
     */
    private async handleStreamingResponse(
        client: Ollama,
        requestOptions: OllamaRequestOptions,
        opts: ChatCompletionOptions,
        providerOptions: OllamaOptions
    ): Promise<ChatResponse> {
        log.info(`Using streaming mode with Ollama client`);

        // Log detailed information about the streaming setup
        log.info(`Ollama streaming details: model=${providerOptions.model}, streamCallback=${opts.streamCallback ? 'provided' : 'not provided'}`);

        // Create streaming request
        const streamingRequest = {
            ...requestOptions,
            stream: true as const
        };

        // Handle direct streamCallback if provided
        if (opts.streamCallback) {
            try {
                // Perform health check before streaming
                await performProviderHealthCheck(
                    async () => await client.list(),
                    this.getName()
                );

                log.info(`Making Ollama streaming request after successful health check`);
                // Get the stream iterator
                const streamIterator = await client.chat(streamingRequest);

                // Process the stream with our reusable utility
                const streamResult = await processProviderStream(
                    streamIterator,
                    {
                        providerName: this.getName(),
                        modelName: providerOptions.model
                    },
                    opts.streamCallback
                );

                // Create the final response after streaming is complete
                return {
                    text: streamResult.completeText,
                    model: providerOptions.model,
                    provider: this.getName(),
                    tool_calls: this.transformToolCalls(streamResult.toolCalls),
                    usage: extractStreamStats(streamResult.finalChunk, this.getName())
                };
            } catch (error) {
                log.error(`Error in Ollama streaming with callback: ${error}`);
                log.error(`Error details: ${error instanceof Error ? error.stack : 'No stack trace available'}`);
                throw error;
            }
        } else {
            // Create a stream handler using our reusable StreamProcessor
            const streamHandler = createStreamHandler(
                {
                    providerName: this.getName(),
                    modelName: providerOptions.model,
                    streamCallback: opts.streamCallback
                },
                async (callback) => {
                    let completeText = '';
                    let chunkCount = 0;
                    
                    // Create a response object that will be updated during streaming
                    const response: ChatResponse = {
                        text: '',
                        model: providerOptions.model,
                        provider: this.getName(),
                        tool_calls: []
                    };

                    try {
                        // Perform health check
                        await performProviderHealthCheck(
                            async () => await client.list(),
                            this.getName()
                        );

                        // Get the stream iterator
                        log.info(`Getting stream iterator from Ollama`);
                        const streamIterator = await client.chat(streamingRequest);

                        if (!streamIterator || typeof streamIterator[Symbol.asyncIterator] !== 'function') {
                            throw new Error('Invalid stream iterator returned');
                        }

                        // Process each chunk using our stream processor
                        for await (const chunk of streamIterator) {
                            chunkCount++;

                            // Process the chunk and update our accumulated text
                            const result = await StreamProcessor.processChunk(
                                chunk,
                                completeText,
                                chunkCount,
                                { providerName: this.getName(), modelName: providerOptions.model }
                            );

                            completeText = result.completeText;

                            // Extract any tool calls
                            const toolCalls = StreamProcessor.extractToolCalls(chunk);
                            // Update response tool calls if any are found
                            if (toolCalls.length > 0) {
                                // Update the response object's tool_calls for final return
                                response.tool_calls = toolCalls;
                            }

                            // Send to callback - directly pass the content without accumulating
                            await callback({
                                text: chunk.message?.content || '',
                                done: false,  // Add done property to satisfy StreamChunk
                                raw: chunk as unknown as Record<string, unknown>
                            });

                            // Log completion
                            if (chunk.done && !result.logged) {
                                log.info(`Reached final chunk after ${chunkCount} chunks, content length: ${completeText.length} chars`);
                            }
                        }

                        return completeText;
                    } catch (error) {
                        log.error(`Error in Ollama streaming: ${error}`);
                        log.error(`Error details: ${error instanceof Error ? error.stack : 'No stack trace available'}`);
                        throw error;
                    }
                }
            );

            // Return a response object with the stream handler
            return {
                text: '', // Initial text is empty, will be populated during streaming
                model: providerOptions.model,
                provider: this.getName(),
                stream: streamHandler as (callback: (chunk: StreamChunk) => Promise<void> | void) => Promise<string>
            };
        }
    }

    /**
     * Transform Ollama tool calls to the standard format expected by the pipeline
     * @param toolCalls Array of tool calls from Ollama response or undefined
     * @returns Standardized ToolCall array for consistent handling in the pipeline
     */
    private transformToolCalls(toolCalls: unknown[] | undefined): ToolCall[] {
        if (!toolCalls || !Array.isArray(toolCalls) || toolCalls.length === 0) {
            return [];
        }

        return toolCalls.map((toolCall, index) => {
            // Use type guards to safely access properties
            const toolCallObj = toolCall as { id?: string; function?: { name?: string; arguments?: string } };
            
            // Generate a unique ID if none is provided
            const id = typeof toolCallObj.id === 'string' ? toolCallObj.id : `tool-call-${Date.now()}-${index}`;
            
            // Safely extract function name and arguments with defaults
            const functionName = toolCallObj.function && typeof toolCallObj.function.name === 'string' 
                ? toolCallObj.function.name 
                : 'unknown_function';
                
            const functionArgs = toolCallObj.function && typeof toolCallObj.function.arguments === 'string'
                ? toolCallObj.function.arguments
                : '{}';

            // Return a properly typed ToolCall object

            return {
                id,
                type: 'function',
                function: {
                    name: functionName,
                    arguments: functionArgs
                }
            };
        });
    }

    /**
     * Adds a system message with feedback about tool execution status
     * @param messages The current message array
     * @param toolExecutionStatus Array of tool execution status objects
     * @returns Updated message array with feedback
     */
    private addToolExecutionFeedback(messages: Message[], toolExecutionStatus: ToolExecutionStatus[]): Message[] {
        if (!toolExecutionStatus || toolExecutionStatus.length === 0) {
            return messages;
        }

        // Create a copy of the messages
        const updatedMessages = [...messages];

        // Create a feedback message that explains what happened with each tool call
        let feedbackContent = `Tool execution feedback:\n\n`;

        toolExecutionStatus.forEach((status, index) => {
            // Add status for each tool
            const statusText = status.success ? 'successfully executed' : 'failed to execute';
            const toolName = status.name || 'unknown tool';

            feedbackContent += `Tool call ${index + 1} (${toolName}): ${statusText}\n`;

            // Add error information if available and tool failed
            if (!status.success && status.error) {
                feedbackContent += `Error: ${status.error}\n`;
                feedbackContent += `Please fix this issue in your next response or try a different approach.\n`;
            }

            feedbackContent += `\n`;
        });

        // Add feedback message to the conversation
        updatedMessages.push({
            role: 'system',
            content: feedbackContent
        });

        log.info(`Added tool execution feedback: ${toolExecutionStatus.length} statuses`);
        return updatedMessages;
    }

    /**
     * Clear cached Ollama client to force recreation with new settings
     */
    clearCache(): void {
        // Ollama service doesn't maintain a persistent client like OpenAI/Anthropic
        // but we can clear any future cached state here if needed
        log.info('Ollama client cache cleared (no persistent client to clear)');
    }
}
