/**
 * OpenAI-compatible adapter — uses Vercel AI SDK (@ai-sdk/openai + ai).
 * Public contract unchanged from the hand-rolled version; works with any
 * OpenAI-compat endpoint (api.openai.com, DeepSeek, OpenRouter, Anyscale,
 * vLLM, Ollama-OpenAI shim, etc.) via the baseUrl override.
 */

import { createOpenAI } from '@ai-sdk/openai';
import { jsonSchema, streamText, type ModelMessage } from 'ai';
import type { ContentBlock, Message } from '../types';
import type { LlmAdapter, LlmCallbacks, LlmRequest, LlmTurnResult, StopReason } from './types';

function historyToModelMessages(history: Message[]): ModelMessage[] {
  const out: ModelMessage[] = [];
  for (const m of history) {
    if (m.role === 'user') {
      const toolResults = m.blocks.filter((b): b is Extract<ContentBlock, { type: 'tool_result' }> => b.type === 'tool_result');
      if (toolResults.length > 0) {
        out.push({
          role: 'tool',
          content: toolResults.map((r) => ({
            type: 'tool-result' as const,
            toolCallId: r.tool_use_id,
            toolName: '',
            output: { type: 'text', value: r.content },
          })),
        });
        continue;
      }
      const text = m.blocks
        .filter((b): b is Extract<ContentBlock, { type: 'text' }> => b.type === 'text')
        .map((b) => b.text).join('\n');
      if (text.length > 0) out.push({ role: 'user', content: text });
    } else {
      const parts: any[] = [];
      for (const b of m.blocks) {
        if (b.type === 'text' && b.text.length > 0) {
          parts.push({ type: 'text', text: b.text });
        } else if (b.type === 'tool_use') {
          parts.push({
            type: 'tool-call',
            toolCallId: b.id,
            toolName: b.name,
            input: b.input ?? {},
          });
        }
      }
      if (parts.length > 0) out.push({ role: 'assistant', content: parts });
    }
  }
  return out;
}

function mapStop(r: string | undefined): StopReason {
  switch (r) {
    case 'stop': return 'end_turn';
    case 'tool-calls': return 'tool_use';
    case 'length': return 'max_tokens';
    case 'content-filter': return 'refusal';
    case 'error': return 'error';
    default: return 'other';
  }
}

export const openaiAdapter: LlmAdapter = {
  async stream(req: LlmRequest, cb: LlmCallbacks): Promise<LlmTurnResult> {
    const client = createOpenAI({
      apiKey: req.apiKey,
      baseURL: req.baseUrl || undefined,
    });

    const tools: Record<string, any> = {};
    for (const t of req.tools) {
      tools[t.name] = {
        description: t.description,
        inputSchema: jsonSchema(t.input_schema),
      };
    }

    // Some providers (litellm/vLLM) reject `tools: []` — they want the
    // field omitted entirely when there are no tools.
    const hasTools = Object.keys(tools).length > 0;
    // Force /v1/chat/completions. @ai-sdk/openai v3 routes the default
    // factory to the Responses API (/v1/responses), which only OpenAI
    // first-party hosts — DeepSeek/Qianwen/LiteLLM proxies/vLLM/Ollama
    // all 400 on it. .chat() picks the universally-supported endpoint.
    const result = streamText({
      model: client.chat(req.model),
      system: req.system,
      messages: historyToModelMessages(req.history),
      ...(hasTools ? { tools } : {}),
      maxOutputTokens: req.maxTokens,
    });

    const content: ContentBlock[] = [];
    let textBuf = '';
    for await (const part of result.fullStream) {
      if (part.type === 'text-delta') {
        textBuf += part.text;
        cb.onTextDelta(part.text);
      } else if (part.type === 'tool-call') {
        if (textBuf.length > 0) {
          content.push({ type: 'text', text: textBuf });
          textBuf = '';
        }
        cb.onToolUse({ id: part.toolCallId, name: part.toolName, input: part.input });
        content.push({ type: 'tool_use', id: part.toolCallId, name: part.toolName, input: part.input });
      } else if (part.type === 'error') {
        throw new Error(`OpenAI stream error: ${String((part as any).error)}`);
      }
    }
    if (textBuf.length > 0) content.push({ type: 'text', text: textBuf });

    const finishReason = await result.finishReason;
    let usage;
    try {
      const u: any = await result.usage;
      if (u) {
        usage = {
          inputTokens: u.inputTokens ?? u.promptTokens,
          outputTokens: u.outputTokens ?? u.completionTokens,
          totalTokens: u.totalTokens,
        };
      }
    } catch {}
    return { stopReason: mapStop(finishReason), content, usage };
  },
};
