const OpenAI = require("openai");
const Provider = require("./ai-provider.js");
const { RetryError } = require("../error.js");

/**
 * The agent provider for the OpenAI API.
 * By default, the model is set to 'gpt-3.5-turbo'.
 */
class OpenAIProvider extends Provider {
  model;
  static COST_PER_TOKEN = {
    "gpt-3.5-turbo": {
      input: 0.0015,
      output: 0.002,
    },
    "gpt-3.5-turbo-16k": {
      input: 0.003,
      output: 0.004,
    },
    "gpt-4": {
      input: 0.03,
      output: 0.06,
    },
    "gpt-4-turbo": {
      input: 0.01,
      output: 0.03,
    },
    "gpt-4o": {
      input: 0.005,
      output: 0.015,
    },
    "gpt-4-32k": {
      input: 0.06,
      output: 0.12,
    },
    "gpt-4o-mini": {
      input: 0.00015,
      output: 0.0006,
    },
  };

  constructor(config = {}) {
    const {
      options = {
        apiKey: process.env.OPEN_AI_KEY,
        maxRetries: 3,
      },
      model = "gpt-4o",
    } = config;

    const client = new OpenAI(options);

    super(client);

    this.model = model;
  }

  /**
   * Create a completion based on the received messages.
   *
   * @param messages A list of messages to send to the OpenAI API.
   * @param functions
   * @returns The completion.
   */
  async complete(messages, functions = []) {
    try {
      const response = await this.client.chat.completions.create({
        model: this.model,
        // stream: true,
        messages,
        ...(Array.isArray(functions) && functions?.length > 0
          ? { functions }
          : {}),
      });

      // Right now, we only support one completion,
      // so we just take the first one in the list
      const completion = response.choices[0].message;
      const cost = this.getCost(response.usage);
      // treat function calls
      if (completion.function_call) {
        let functionArgs = {};
        try {
          functionArgs = JSON.parse(completion.function_call.arguments);
        } catch (error) {
          // call the complete function again in case it gets a json error
          return this.complete(
            [
              ...messages,
              {
                role: "function",
                name: completion.function_call.name,
                function_call: completion.function_call,
                content: error?.message,
              },
            ],
            functions
          );
        }

        // console.log(completion, { functionArgs })
        return {
          result: null,
          functionCall: {
            name: completion.function_call.name,
            arguments: functionArgs,
          },
          cost,
        };
      }

      return {
        result: completion.content,
        cost,
      };
    } catch (error) {
      // If invalid Auth error we need to abort because no amount of waiting
      // will make auth better.
      if (error instanceof OpenAI.AuthenticationError) throw error;

      if (
        error instanceof OpenAI.RateLimitError ||
        error instanceof OpenAI.InternalServerError ||
        error instanceof OpenAI.APIError // Also will catch AuthenticationError!!!
      ) {
        throw new RetryError(error.message);
      }

      throw error;
    }
  }

  /**
   * Get the cost of the completion.
   *
   * @param usage The completion to get the cost for.
   * @returns The cost of the completion.
   */
  getCost(usage) {
    if (!usage) {
      return Number.NaN;
    }

    // regex to remove the version number from the model
    const modelBase = this.model.replace(/-(\d{4})$/, "");

    if (!(modelBase in OpenAIProvider.COST_PER_TOKEN)) {
      return Number.NaN;
    }

    const costPerToken = OpenAIProvider.COST_PER_TOKEN?.[modelBase];
    const inputCost = (usage.prompt_tokens / 1000) * costPerToken.input;
    const outputCost = (usage.completion_tokens / 1000) * costPerToken.output;

    return inputCost + outputCost;
  }
}

module.exports = OpenAIProvider;
