import { ChatMessage, ChatOptions, ProviderResponse } from '../../../types/chat';
import { ProviderCapabilities, EmbeddingProvider } from '../../../types/provider';
import { Tool, GenericToolSchema, StandardizedToolCall } from '../../../types/tool';
import { ILogger } from '../../../types/common';
import { OpenAIProviderConfig } from './types';
import { OpenAIConfig } from './config';
import { chat } from './chat';
import { streamChat } from './streaming';
import { embed, embedBatch } from './embedding';
import { convertToolSchema, convertToolCall } from './tools';
import { BaseProvider } from '../BaseProvider';
import { OpenAICache } from './cache';
import { OpenAIProviderError, handleOpenAIError } from './errors';

/**
 * OpenAIProvider class that extends BaseProvider and implements EmbeddingProvider.
 * This provider interacts with the OpenAI API for various AI-related tasks.
 */
export class OpenAIProvider extends BaseProvider implements EmbeddingProvider {
  private config: OpenAIConfig;
  private cache: OpenAICache;

  /**
   * Creates an instance of OpenAIProvider.
   * @param {OpenAIProviderConfig} config - The configuration for the OpenAI provider.
   * @param {ILogger} logger - The logger instance for logging.
   */
  constructor(config: OpenAIProviderConfig, logger: ILogger) {
    super(logger);
    this.config = new OpenAIConfig(config, logger);
    this.cache = new OpenAICache();
  }

  /**
   * Sends a chat request to the OpenAI API.
   * @param {ChatMessage[]} messages - The chat messages to send.
   * @param {ChatOptions} options - The options for the chat request.
   * @param {Tool[]} [tools] - Optional tools to use in the chat.
   * @returns {Promise<ProviderResponse>} The response from the API.
   * @throws {OpenAIProviderError} If there's an error during the API call.
   */
  async chat(messages: ChatMessage[], options: ChatOptions, tools?: Tool[]): Promise<ProviderResponse> {
    try {
      const cacheKey = { messages, options, tools };
      if (this.cache.has(cacheKey)) {
        this.logger.debug('Cache hit for chat request');
        return this.cache.get(cacheKey)!;
      }

      const processedMessages = await this.applyMiddleware(messages);
      const response = await chat(this.config, processedMessages, options, tools);
      const processedResponse = await this.applyMiddlewareToResponse(response);
      if (processedResponse.toolCalls) {
        processedResponse.toolCalls = await this.applyMiddlewareToToolCalls(processedResponse.toolCalls);
      }

      this.cache.set(cacheKey, processedResponse);
      return processedResponse;
    } catch (error: unknown) {
      this.logger.error('Error in OpenAIProvider chat', { error: error instanceof Error ? error.message : String(error) });
      throw handleOpenAIError(error);
    }
  }

  /**
   * Sends a streaming chat request to the OpenAI API.
   * @param {ChatMessage[]} messages - The chat messages to send.
   * @param {ChatOptions} options - The options for the chat request.
   * @param {Tool[]} [tools] - Optional tools to use in the chat.
   * @returns {AsyncIterableIterator<ProviderResponse>} An async iterator of responses from the API.
   * @throws {OpenAIProviderError} If there's an error during the API call.
   */
  async *streamChat(messages: ChatMessage[], options: ChatOptions, tools?: Tool[]): AsyncIterableIterator<ProviderResponse> {
    try {
      const processedMessages = await this.applyMiddleware(messages);
      for await (const response of streamChat(this.config, processedMessages, options, tools)) {
        const processedResponse = await this.applyMiddlewareToResponse(response);
        if (processedResponse.toolCalls) {
          processedResponse.toolCalls = await this.applyMiddlewareToToolCalls(processedResponse.toolCalls);
        }
        yield processedResponse;
      }
    } catch (error: unknown) {
      this.logger.error('Error in OpenAIProvider streamChat', { error: error instanceof Error ? error.message : String(error) });
      throw handleOpenAIError(error);
    }
  }

  /**
   * Returns the capabilities of the OpenAI provider.
   * @returns {ProviderCapabilities} The capabilities of the provider.
   */
  getCapabilities(): ProviderCapabilities {
    return {
      maxTokens: 4096,
      supportsFunctionCalling: true,
      supportsStreaming: true,
      supportedModels: ['gpt-4o', 'o1-preview'],
      maxSimultaneousCalls: 1,
      supportsSemanticCaching: false,
    };
  }

  /**
   * Generates an embedding for the given text.
   * @param {string} text - The text to embed.
   * @returns {Promise<number[]>} The embedding as an array of numbers.
   * @throws {OpenAIProviderError} If there's an error during the API call.
   */
  async embed(text: string): Promise<number[]> {
    try {
      return await embed(this.config, text);
    } catch (error: unknown) {
      this.logger.error('Error in OpenAIProvider embed', { error: error instanceof Error ? error.message : String(error) });
      throw handleOpenAIError(error);
    }
  }

  /**
   * Generates embeddings for the given texts.
   * @param {string[]} texts - The texts to embed.
   * @returns {Promise<number[][]>} The embeddings as an array of number arrays.
   * @throws {OpenAIProviderError} If there's an error during the API call.
   */
  async embedBatch(texts: string[]): Promise<number[][]> {
    try {
      return await embedBatch(this.config, texts);
    } catch (error: unknown) {
      this.logger.error('Error in OpenAIProvider embedBatch', { error: error instanceof Error ? error.message : String(error) });
      throw handleOpenAIError(error);
    }
  }

  /**
   * Converts a generic tool schema to the OpenAI-specific format.
   * @param {GenericToolSchema} schema - The generic tool schema to convert.
   * @returns {any} The converted schema in OpenAI format.
   */
  convertToolSchema(schema: GenericToolSchema): any {
    return convertToolSchema(schema);
  }

  /**
   * Converts an OpenAI tool call to the standardized format.
   * @param {any} call - The OpenAI tool call to convert.
   * @returns {StandardizedToolCall} The converted tool call in standardized format.
   */
  convertToolCall(call: any): StandardizedToolCall {
    return convertToolCall(call);
  }

  /**
   * Clears the internal cache of the provider.
   */
  clearCache(): void {
    this.cache.clear();
    this.logger.debug('OpenAIProvider cache cleared');
  }
}

export * from './types';
export { OpenAIProviderError } from './errors';