const { v4 } = require("uuid");
const { safeJsonParse } = require("../../../../http");

/**
 * Shared native OpenAI-compatible tool calling utilities.
 * Any provider with an OpenAI-compatible client can use these functions
 * instead of the UnTooled prompt-based approach when the model supports
 * native tool calling.
 *
 * Usage in a provider:
 *   const { tooledStream, tooledComplete } = require("./helpers/tooled.js");
 *
 *   async stream(messages, functions, eventHandler) {
 *     if (functions.length > 0 && await this.supportsNativeToolCalling()) {
 *       return tooledStream(this.client, this.model, messages, functions, eventHandler);
 *     }
 *     // ... fallback to UnTooled ...
 *   }
 */

/**
 * Convert aibitat function definitions to the OpenAI tools format.
 * @param {Array<{name: string, description: string, parameters: object}>} functions
 * @returns {Array<{type: "function", function: {name: string, description: string, parameters: object}}>}
 */
function formatFunctionsToTools(functions) {
  if (!Array.isArray(functions) || functions.length === 0) return [];
  return functions.map((func) => ({
    type: "function",
    function: {
      name: func.name,
      description: func.description,
      parameters: func.parameters,
    },
  }));
}

/**
 * Format message content with attachments (images) for multimodal support.
 * Transforms a message with attachments into the OpenAI-compatible format.
 * @param {Object} message - The message to format
 * @returns {Object} Message with content formatted for the API
 */
function formatMessageWithAttachments(message) {
  if (!message.attachments || message.attachments.length === 0) {
    return message;
  }

  // Transform message with attachments into multimodal format
  const content = [{ type: "text", text: message.content }];
  for (const attachment of message.attachments) {
    content.push({
      type: "image_url",
      image_url: {
        url: attachment.contentString,
      },
    });
  }

  // Return message without attachments property, with content as array
  const { attachments: _, ...rest } = message;
  return {
    ...rest,
    content,
  };
}

/**
 * Convert the aibitat message history (which uses role:"function" with
 * `originalFunctionCall` metadata) into the OpenAI tool-calling message
 * format (assistant `tool_calls` + role:"tool" pairs).
 * Also handles image attachments for multimodal support.
 * @param {Array} messages
 * @param {{injectReasoningContent?: boolean}} options
 *   - injectReasoningContent: when true, ensures every assistant message has
 *     a `reasoning_content` field (required by DeepSeek thinking-mode models).
 * @returns {Array} Messages formatted for the OpenAI tools API
 */
function formatMessagesForTools(messages, options = {}) {
  const formattedMessages = [];
  const { injectReasoningContent = false } = options;

  for (const message of messages) {
    if (message.role === "function") {
      if (message.originalFunctionCall?.id) {
        const prevMsg = formattedMessages[formattedMessages.length - 1];
        if (!prevMsg || prevMsg.role !== "assistant" || !prevMsg.tool_calls) {
          formattedMessages.push({
            role: "assistant",
            content: null,
            ...(injectReasoningContent ? { reasoning_content: "" } : {}),
            tool_calls: [
              {
                id: message.originalFunctionCall.id,
                type: "function",
                function: {
                  name: message.originalFunctionCall.name,
                  arguments:
                    typeof message.originalFunctionCall.arguments === "string"
                      ? message.originalFunctionCall.arguments
                      : JSON.stringify(message.originalFunctionCall.arguments),
                },
              },
            ],
          });
        }
        formattedMessages.push({
          role: "tool",
          tool_call_id: message.originalFunctionCall.id,
          content:
            typeof message.content === "string"
              ? message.content
              : JSON.stringify(message.content),
        });
      } else {
        const toolCallId = `call_${v4()}`;
        formattedMessages.push({
          role: "assistant",
          content: null,
          ...(injectReasoningContent ? { reasoning_content: "" } : {}),
          tool_calls: [
            {
              id: toolCallId,
              type: "function",
              function: {
                name: message.name,
                arguments: "{}",
              },
            },
          ],
        });
        formattedMessages.push({
          role: "tool",
          tool_call_id: toolCallId,
          content:
            typeof message.content === "string"
              ? message.content
              : JSON.stringify(message.content),
        });
      }
    } else if (
      injectReasoningContent &&
      message.role === "assistant" &&
      !("reasoning_content" in message)
    ) {
      formattedMessages.push(
        formatMessageWithAttachments({ ...message, reasoning_content: "" })
      );
    } else {
      formattedMessages.push(formatMessageWithAttachments(message));
    }
  }

  return formattedMessages;
}

/**
 * Stream a chat completion using native OpenAI-compatible tool calling.
 * Handles parallel tool calls by tracking each tool call by its streaming
 * index, then returning only the first one for the agent framework to process.
 *
 * @param {import("openai").OpenAI} client - OpenAI-compatible client
 * @param {string} model - Model identifier
 * @param {Array} messages - Raw aibitat message history
 * @param {Array} functions - Aibitat function definitions
 * @param {function|null} eventHandler - Stream event handler
 * @param {{injectReasoningContent?: boolean, provider?: object}} options - Provider-specific options
 *   - provider: If passed, automatically handles usage tracking via provider.resetUsage()/recordUsage()
 * @returns {Promise<{textResponse: string, functionCall: object|null, uuid: string, usage: object|null}>}
 */
async function tooledStream(
  client,
  model,
  messages,
  functions = [],
  eventHandler = null,
  options = {}
) {
  const { provider, ...formatOptions } = options;

  // Auto-reset usage if provider is passed
  if (provider?.resetUsage) {
    try {
      provider.resetUsage();
    } catch {}
  }

  const msgUUID = v4();
  const formattedMessages = formatMessagesForTools(messages, formatOptions);
  const tools = formatFunctionsToTools(functions);

  const stream = await client.chat.completions.create({
    model,
    stream: true,
    stream_options: { include_usage: true },
    messages: formattedMessages,
    ...(tools.length > 0 ? { tools } : {}),
  });

  const result = {
    functionCall: null,
    textResponse: "",
  };

  const toolCallsByIndex = {};
  let usage = null;
  let time_info = null;

  for await (const chunk of stream) {
    // Capture usage from final chunk (some providers send usage after finish_reason)
    if (chunk?.usage) usage = chunk.usage;
    if (chunk?.time_info) time_info = chunk.time_info;

    if (!chunk?.choices?.[0]) continue;
    const choice = chunk.choices[0];

    if (choice.delta?.content) {
      result.textResponse += choice.delta.content;
      eventHandler?.("reportStreamEvent", {
        type: "textResponseChunk",
        uuid: msgUUID,
        content: choice.delta.content,
      });
    }

    if (choice.delta?.tool_calls) {
      for (const toolCall of choice.delta.tool_calls) {
        const idx = toolCall.index ?? 0;

        // Initialize tool call entry if it doesn't exist yet.
        // Some providers (e.g. mlx-server) send id as null, so we generate one.
        if (!toolCallsByIndex[idx]) {
          toolCallsByIndex[idx] = {
            id: toolCall.id || `call_${v4()}`,
            name: toolCall.function?.name || "",
            arguments: toolCall.function?.arguments || "",
          };
        } else {
          // Update existing entry with streamed data
          if (toolCall.id && !toolCallsByIndex[idx].id.startsWith("call_")) {
            toolCallsByIndex[idx].id = toolCall.id;
          }
          if (toolCall.function?.name) {
            toolCallsByIndex[idx].name += toolCall.function.name;
          }
          if (toolCall.function?.arguments) {
            toolCallsByIndex[idx].arguments += toolCall.function.arguments;
          }
        }

        if (toolCallsByIndex[idx]) {
          eventHandler?.("reportStreamEvent", {
            uuid: `${msgUUID}:tool_call_invocation`,
            type: "toolCallInvocation",
            content: `Assembling Tool Call: ${toolCallsByIndex[idx].name}(${toolCallsByIndex[idx].arguments})`,
          });
        }
      }
    }
  }

  // Auto-record usage if provider is passed and usage is available
  if (provider?.recordUsage && usage) {
    try {
      provider.recordUsage(usage, time_info);
    } catch {}
  }

  const toolCallIndices = Object.keys(toolCallsByIndex).map(Number);
  if (toolCallIndices.length > 0) {
    const firstToolCall = toolCallsByIndex[Math.min(...toolCallIndices)];
    result.functionCall = {
      id: firstToolCall.id,
      name: firstToolCall.name,
      arguments: safeJsonParse(firstToolCall.arguments, {}),
    };
  }

  return {
    textResponse: result.textResponse,
    functionCall: result.functionCall,
    uuid: msgUUID,
    usage,
  };
}

/**
 * Non-streaming chat completion using native OpenAI-compatible tool calling.
 * Returns the first tool call if the model requests any, otherwise the text response.
 *
 * @param {import("openai").OpenAI} client - OpenAI-compatible client
 * @param {string} model - Model identifier
 * @param {Array} messages - Raw aibitat message history
 * @param {Array} functions - Aibitat function definitions
 * @param {function} getCostFn - Provider's getCost function
 * @param {{injectReasoningContent?: boolean, provider?: object}} options - Provider-specific options
 *   - provider: If passed, automatically handles usage tracking via provider.resetUsage()/recordUsage()
 * @returns {Promise<{textResponse: string|null, functionCall: object|null, cost: number, usage: object|null}>}
 */
async function tooledComplete(
  client,
  model,
  messages,
  functions = [],
  getCostFn = () => 0,
  options = {}
) {
  const { provider, ...formatOptions } = options;

  // Auto-reset usage if provider is passed
  if (provider?.resetUsage) {
    try {
      provider.resetUsage();
    } catch {}
  }

  const formattedMessages = formatMessagesForTools(messages, formatOptions);
  const tools = formatFunctionsToTools(functions);

  const response = await client.chat.completions.create({
    model,
    stream: false,
    messages: formattedMessages,
    ...(tools.length > 0 ? { tools } : {}),
  });

  const completion = response.choices[0].message;
  const cost = getCostFn(response.usage);
  const usage = response.usage || null;

  // Auto-record usage if provider is passed and usage is available
  if (provider?.recordUsage && usage) {
    try {
      provider.recordUsage(usage);
    } catch {}
  }

  if (completion.tool_calls && completion.tool_calls.length > 0) {
    const toolCall = completion.tool_calls[0];
    const functionArgs = safeJsonParse(toolCall.function.arguments, null);

    if (functionArgs === null) {
      return {
        textResponse: null,
        retryWithError: {
          role: "function",
          name: toolCall.function.name,
          content: `Failed to parse tool call arguments as JSON. Raw arguments: ${toolCall.function.arguments}`,
          originalFunctionCall: {
            id: toolCall.id,
            name: toolCall.function.name,
            arguments: toolCall.function.arguments,
          },
        },
        cost,
        usage,
      };
    }

    return {
      textResponse: null,
      functionCall: {
        id: toolCall.id,
        name: toolCall.function.name,
        arguments: functionArgs,
      },
      cost,
      usage,
    };
  }

  return {
    textResponse: completion.content,
    cost,
    usage,
  };
}

module.exports = {
  formatFunctionsToTools,
  formatMessagesForTools,
  tooledStream,
  tooledComplete,
};
