import logger from '../logger';
import { OpenAI } from 'openai';
import { ChatCompletionMessageParam } from 'openai/resources';
import { AIConfig, CONFIG } from '../config';
import { ChatCompletion } from 'openai/src/resources/chat/completions';
import { Tool } from "openai/resources/responses/responses";
import NodeCache from "node-cache";
import { AIRole, AIService, ToolExecutionContext } from "../interfaces/ai-interfaces";
import { cleanChatCompletionMessage, countMessages, sanitizeForLog, trimCachePreserveMessageStart } from "../utils";
import Roboto from "../bot/roboto";
import { ChatConfiguration } from "../config/chat-configurations";

class CustomOpenAIService implements AIService<ChatCompletionMessageParam, any> {

  private messagesCache = new NodeCache({
    stdTTL: CONFIG.BotConfig.messageCacheTtl || CONFIG.BotConfig.nodeCacheTime,
    checkperiod: 600,
  });
  private static clientCache = new Map<string, OpenAI>();

  constructor() {
  }

  private getClient(baseURL?: string, apiKey?: string): OpenAI {
    const key = `${baseURL ?? ''}::${apiKey ?? ''}`;
    let client = CustomOpenAIService.clientCache.get(key);
    if (!client) {
      client = new OpenAI({ baseURL, apiKey });
      CustomOpenAIService.clientCache.set(key, client);
    }
    return client;
  }

  public deleteChatCache(chatId: string){
    this.messagesCache.del(chatId);
  }

  public addMessageToCache(item: ChatCompletionMessageParam, chatId: string){
    const aiMessages: ChatCompletionMessageParam[] = this.messagesCache.get(chatId) || [];
    aiMessages.push(item);
    this.messagesCache.set(chatId, aiMessages, CONFIG.BotConfig.nodeCacheTime);
  }

  public hasChatCache(chatId: string): boolean {
    return this.messagesCache.has(chatId);
  }

  public async sendMessage(aiMessagesInputList: ChatCompletionMessageParam[], systemPrompt: string, chatConfig: ChatConfiguration, tools: any, toolContext?: ToolExecutionContext): Promise<string> {
    let cycleCount = 0;
    const maxCycles = 5;
    const chatId = chatConfig.chatId;

    const aiMessages: any[]  = this.messagesCache.get(chatId) || [];
    aiMessages.push(...aiMessagesInputList)

    while (cycleCount < maxCycles) {
      const aiResponse: OpenAI.ChatCompletionMessage = await this.sendCompletion(aiMessages, 'text', tools, systemPrompt);

      const tool_calls = aiResponse.tool_calls || [];
      let hasFunctionCall = tool_calls.length > 0;
      const functionOutputs = [];

      for (const output of tool_calls) {

        if (output.type == 'function') {
          aiMessages.push(cleanChatCompletionMessage(aiResponse));

          hasFunctionCall = true;
          const functionResult = await Roboto.handleFunction(output.function.name, output.function.arguments, toolContext);

          functionOutputs.push({role: "tool", tool_call_id: output.id, content: JSON.stringify(functionResult)})

        } else {
          logger.error(`[CustomOpenAIService] Unknown output type received : "${output.type}". Please report this issue.`);
        }
      }

      aiMessages.push(...functionOutputs);

      cycleCount += 1;

      if (!hasFunctionCall) {
        aiMessages.push(cleanChatCompletionMessage(aiResponse));

        const finalMsgList = trimCachePreserveMessageStart(aiMessages, chatConfig.maxMsgsLimit ?? 30);
        this.messagesCache.set(chatId, finalMsgList, CONFIG.BotConfig.nodeCacheTime);
        return aiResponse.content;
      }
    }

    throw new Error(`Reached the limit of ${maxCycles} communication cycles with OpenAI.`);
  }

  /**
   * Sends a series of messages to the OpenAI Chat Completion API and retrieves a generated completion.
   * This function is designed to interact with the OpenAI API, sending it a context composed of several messages.
   * It then receives a response that is generated based on this context, aiming to provide a coherent and contextually appropriate continuation or reply.
   *
   * Parameters:
   * - messageList: An array of ChatCompletionMessageParam objects, which include the messages that form the context for the API request.
   *
   * Returns:
   * - A promise that resolves to the generated completion string, which is the API's response based on the provided context.
   */
  async sendCompletion(
      messageList: ChatCompletionMessageParam[],
      responseType: 'json_object'|'text' = 'text',
      tools: Array<Tool>,
      systemPrompt?: string
  ): Promise<OpenAI.ChatCompletionMessage> {

    const client = this.getClient(AIConfig.ChatConfig.baseURL, AIConfig.ChatConfig.apiKey);

    const hasSystemMsg = (messageList[0] as any).role == AIRole.SYSTEM;
    if(systemPrompt) {
      if(hasSystemMsg) messageList.shift();
      messageList.unshift({role: AIRole.SYSTEM, content: systemPrompt});
    }

    logger.info(`[${AIConfig.ChatConfig.provider}] Sending ${countMessages(messageList)} messages`);
    logger.debug(`[${AIConfig.ChatConfig.provider}] Sending Msg: ${JSON.stringify(sanitizeForLog(messageList[messageList.length - 1]))}`);

    const params: any = {
      model: AIConfig.ChatConfig.model,
      messages: messageList,
      reasoning_effort: AIConfig.ChatConfig.reasoningEffort as any,
      response_format: {
        type: responseType
      },
      tools: tools,
      store: CONFIG.BotConfig.openAIStore
    }
    const response: ChatCompletion = await client.chat.completions.create(params);

    logger.debug(`[${AIConfig.ChatConfig.provider}] ResponsesAPI Usage: Input=${response.usage?.prompt_tokens}` + ` Cached=${response.usage?.prompt_tokens_details?.cached_tokens}` + ` Output=${response.usage?.completion_tokens}`);
    logger.debug(`[${AIConfig.ChatConfig.provider}] ResponsesAPI Response:` + JSON.stringify(sanitizeForLog(response.choices[0].message)));

    return response.choices[0].message;
  }

  async generateImage(prompt) {

    const client = this.getClient(AIConfig.ImageConfig.baseURL, AIConfig.ImageConfig.apiKey);

    logger.debug(`[${AIConfig.ImageConfig.provider}->generateImage] Creating image (prompt: ${sanitizeForLog(prompt)?.substring(0, 100) ?? 'N/A'})`);

    const response = await client.images.generate({
      prompt: prompt,
      model: AIConfig.ImageConfig.model,
      n: 1,
      size: "1024x1024"
    });

    return response.data;
  }


}

const CustomOpenAISvc = new CustomOpenAIService();

export default CustomOpenAISvc;
