import OpenAI from 'openai';
import type { ChatCompletionChunk } from 'openai/resources/chat/completions.js';
import {
  LLMProvider,
  LLMMessage,
  LLMResponse,
  LLMProviderConfig,
  ToolCall,
  LLMChatCallbacks,
} from '../plugin_entry.js';
import { AnyAgentTool } from '../../tools/common.js';
import { buildOpenAIThinkingParams } from '../../runtime/thinking.js';
import { extractThinkTagContent, logProviderResponse } from './logging.js';

function isMiniMaxChannel(config: LLMProviderConfig): boolean {
  return config.channelSupplier?.toLowerCase() === 'minimax';
}

function stripThinkTags(content: string): string {
  return content.replace(/<think>[\s\S]*?<\/think>/gi, '').trimStart();
}

function partialTagPrefixLength(value: string, tag: string): number {
  const max = Math.min(value.length, tag.length - 1);
  const lowerValue = value.toLowerCase();
  const lowerTag = tag.toLowerCase();
  for (let len = max; len > 0; len--) {
    if (lowerValue.slice(-len) === lowerTag.slice(0, len)) {
      return len;
    }
  }
  return 0;
}

function createThinkTagStreamParser(callbacks?: LLMChatCallbacks) {
  let mode: 'content' | 'thinking' = 'content';
  let pending = '';
  let content = '';
  let thinking = '';
  let hasThinkTag = false;

  const emitContent = (text: string) => {
    if (!text) return;
    content += text;
    callbacks?.onTextDelta?.(text);
  };
  const emitThinking = (text: string) => {
    if (!text) return;
    thinking += text;
    callbacks?.onThinkingDelta?.(text);
  };

  const consume = (text: string, flush = false) => {
    pending += text;

    while (pending) {
      const tag = mode === 'content' ? '<think>' : '</think>';
      const lowerPending = pending.toLowerCase();
      const tagIndex = lowerPending.indexOf(tag);

      if (tagIndex >= 0) {
        const beforeTag = pending.slice(0, tagIndex);
        if (mode === 'content') emitContent(beforeTag);
        else emitThinking(beforeTag);

        pending = pending.slice(tagIndex + tag.length);
        hasThinkTag = true;
        mode = mode === 'content' ? 'thinking' : 'content';
        continue;
      }

      if (flush) {
        if (mode === 'content') emitContent(pending);
        else emitThinking(pending);
        pending = '';
        break;
      }

      const keep = partialTagPrefixLength(pending, tag);
      const emitText = keep > 0 ? pending.slice(0, -keep) : pending;
      if (mode === 'content') emitContent(emitText);
      else emitThinking(emitText);
      pending = keep > 0 ? pending.slice(-keep) : '';
      break;
    }
  };

  return {
    consume,
    finish: () => consume('', true),
    getContent: () => content,
    getThinking: () => thinking,
    hasThinkTag: () => hasThinkTag,
  };
}

export class OpenAIProvider implements LLMProvider {
  id = 'openai';
  name = 'OpenAI';

  async chat(
    messages: LLMMessage[],
    tools: AnyAgentTool[],
    config: LLMProviderConfig,
    callbacks?: LLMChatCallbacks
  ): Promise<LLMResponse> {
    const client = new OpenAI({ apiKey: config.apiKey, baseURL: config.baseUrl });

    const openaiMessages = messages.map(m => this.toOpenAIMessage(m));
    const openaiTools = tools.map(t => ({
      type: 'function' as const,
      function: { name: t.name, description: t.description, parameters: t.parameters as Record<string, unknown> },
    }));

    const createParams: OpenAI.Chat.ChatCompletionCreateParamsStreaming = {
      model: config.model,
      messages: openaiMessages,
      tools: openaiTools.length > 0 ? openaiTools : undefined,
      max_tokens: config.maxTokens ?? 16384,
      temperature: config.temperature,
      stream: true,
      stream_options: { include_usage: true },
    };

    const thinkParams = buildOpenAIThinkingParams(config.thinkLevel || 'off');
    if (thinkParams) {
      (createParams as any).reasoning_effort = thinkParams.reasoning_effort;
    }

    const stream = await client.chat.completions.create(createParams);

    const minimaxThinkTagParser = isMiniMaxChannel(config)
      ? createThinkTagStreamParser(callbacks)
      : null;
    let content = '';
    let thinking = '';
    let usage: LLMResponse['usage'];
    const toolCallsByIndex = new Map<number, ToolCall & { partialArguments: string }>();
    let responseId: string | undefined;
    let stopReason: string | null | undefined;

    for await (const chunk of stream) {
      const typedChunk = chunk as ChatCompletionChunk;
      responseId ||= typedChunk.id;
      if (typedChunk.usage) {
        usage = {
          inputTokens: typedChunk.usage.prompt_tokens ?? 0,
          outputTokens: typedChunk.usage.completion_tokens ?? 0,
        };
      }

      const choice = Array.isArray(typedChunk.choices) ? typedChunk.choices[0] : undefined;
      if (!choice) continue;
      if (choice.finish_reason) stopReason = choice.finish_reason;

      const delta = choice.delta;
      if (!delta) continue;

      if (delta.content) {
        if (minimaxThinkTagParser) {
          minimaxThinkTagParser.consume(delta.content);
        } else {
          content += delta.content;
          callbacks?.onTextDelta?.(delta.content);
        }
      }

      const reasoningDelta =
        (delta as any).reasoning_content ||
        (delta as any).reasoning ||
        (delta as any).reasoning_text ||
        '';

      if (reasoningDelta) {
        thinking += reasoningDelta;
        callbacks?.onThinkingDelta?.(reasoningDelta);
      }

      if (Array.isArray(delta.tool_calls)) {
        for (const toolCallDelta of delta.tool_calls) {
          const index = toolCallDelta.index ?? 0;
          const existing = toolCallsByIndex.get(index) || {
            id: toolCallDelta.id || '',
            name: toolCallDelta.function?.name || '',
            arguments: '{}',
            partialArguments: '',
          };

          if (toolCallDelta.id) existing.id = toolCallDelta.id;
          if (toolCallDelta.function?.name) existing.name = toolCallDelta.function.name;
          if (toolCallDelta.function?.arguments) {
            existing.partialArguments += toolCallDelta.function.arguments;
          }

          toolCallsByIndex.set(index, existing);
        }
      }
    }

    if (minimaxThinkTagParser) {
      minimaxThinkTagParser.finish();
      content = minimaxThinkTagParser.getContent();
      thinking += minimaxThinkTagParser.getThinking();
    }

    const toolCalls: ToolCall[] = [...toolCallsByIndex.entries()]
      .sort((a, b) => a[0] - b[0])
      .map(([, toolCall]) => ({
        id: toolCall.id,
        name: toolCall.name,
        arguments: toolCall.partialArguments || '{}',
      }));

    const thinkTagExtraction = extractThinkTagContent(content);
    if (!thinking && thinkTagExtraction.thinking) {
      thinking = thinkTagExtraction.thinking;
    }
    if (thinkTagExtraction.hasThinkTag) {
      content = stripThinkTags(content);
    }

    logProviderResponse({
      providerId: this.id,
      providerName: this.name,
      channelSupplier: config.channelSupplier,
      channelId: config.channelId,
      channelName: config.channelName,
      model: config.model,
      baseUrl: config.baseUrl,
      thinkLevel: config.thinkLevel,
      rawResponse: {
        id: responseId,
        model: config.model,
        usage,
        finish_reason: stopReason,
        message: {
          role: 'assistant',
          content,
          thinking,
          tool_calls: toolCalls,
        },
      },
      parsed: {
        content,
        thinking,
        thinkSource: thinking ? 'stream_delta' : undefined,
        hasThinkTag: thinkTagExtraction.hasThinkTag || minimaxThinkTagParser?.hasThinkTag(),
        toolCallCount: toolCalls.length,
        stopReason: stopReason ?? undefined,
      },
    });

    return {
      content,
      thinking: thinking || undefined,
      toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
      usage,
      stopReason: stopReason === 'tool_calls' ? 'tool_use' : 'end_turn',
    };
  }

  private toOpenAIMessage(msg: LLMMessage): OpenAI.ChatCompletionMessageParam {
    if (msg.role === 'tool') {
      return { role: 'tool', content: msg.content, tool_call_id: msg.toolCallId! };
    }
    if (msg.role === 'assistant' && msg.toolCalls?.length) {
      return {
        role: 'assistant',
        content: msg.content || null,
        tool_calls: msg.toolCalls.map(tc => ({
          id: tc.id,
          type: 'function' as const,
          function: { name: tc.name, arguments: tc.arguments },
        })),
      };
    }
    return { role: msg.role, content: msg.content } as OpenAI.ChatCompletionMessageParam;
  }
}
