/**
 * OpenAI adapter — system-ai.
 * Supports gpt-4o, o1 etc. Maps AIContentBlock images to OpenAI image_url data-URI form.
 */
import OpenAI from 'openai'
import type { AIAdapter, AIContentBlock, AIMessage, AIRequest, AIResponse } from '../adapter'
import { AIProviderError } from '../errors'
import type { AIEnv } from './index'

type OpenAIContent = string | OpenAI.ChatCompletionContentPart[]

function mapContentBlocks(content: string | AIContentBlock[]): OpenAIContent {
  if (typeof content === 'string') return content
  return content
    .filter((b): b is AIContentBlock => b.type === 'text' || b.type === 'image')
    .map((b): OpenAI.ChatCompletionContentPart => {
      if (b.type === 'text') {
        return { type: 'text', text: b.text ?? '' }
      }
      // image block → OpenAI image_url as data URI
      const img = b.image!
      return {
        type: 'image_url',
        image_url: {
          url: `data:${img.mediaType};base64,${img.data}`,
        },
      }
    })
}

function mapMessages(messages: AIMessage[]): OpenAI.ChatCompletionMessageParam[] {
  return messages.map((m): OpenAI.ChatCompletionMessageParam => {
    const content = mapContentBlocks(m.content)
    if (m.role === 'system') {
      return { role: 'system', content: typeof content === 'string' ? content : JSON.stringify(content) }
    }
    if (m.role === 'assistant') {
      return { role: 'assistant', content: typeof content === 'string' ? content : JSON.stringify(content) }
    }
    return { role: 'user', content }
  })
}

function normalizeError(err: unknown, provider: string): AIProviderError {
  if (err instanceof OpenAI.APIError) {
    const status = err.status
    if (status === 429) {
      return new AIProviderError('rate_limit', provider, true, `Rate limited: ${err.message}`)
    }
    if (status === 401 || status === 403) {
      return new AIProviderError('auth_error', provider, false, `Auth error: ${err.message}`)
    }
    if (status === 404) {
      return new AIProviderError('model_unavailable', provider, true, `Model unavailable: ${err.message}`)
    }
    return new AIProviderError('unknown', provider, false, `API error ${status}: ${err.message}`)
  }
  if (err instanceof Error && (err.name === 'AbortError' || err.message.includes('timeout'))) {
    return new AIProviderError('timeout', provider, true, `Request timed out: ${err.message}`)
  }
  const msg = err instanceof Error ? err.message : String(err)
  return new AIProviderError('unknown', provider, false, `Unknown error: ${msg}`)
}

export class OpenAIAdapter implements AIAdapter {
  readonly provider = 'openai' as const
  private client: OpenAI

  constructor(env: AIEnv) {
    this.client = new OpenAI({ apiKey: env.OPENAI_API_KEY })
  }

  async call(request: AIRequest): Promise<AIResponse> {
    const start = Date.now()
    try {
      const messages = mapMessages(request.messages)

      const response = await this.client.chat.completions.create({
        model: request.model,
        messages,
        ...(request.maxTokens !== undefined ? { max_tokens: request.maxTokens } : {}),
        ...(request.temperature !== undefined ? { temperature: request.temperature } : {}),
      })

      const content = response.choices
        .map((c) => c.message.content ?? '')
        .join('')

      const usage = response.usage

      return {
        content,
        inputTokens: usage?.prompt_tokens ?? 0,
        outputTokens: usage?.completion_tokens ?? 0,
        model: request.model,
        provider: this.provider,
        durationMs: Date.now() - start,
      }
    } catch (err) {
      throw normalizeError(err, this.provider)
    }
  }

  stream(request: AIRequest): ReadableStream<string> {
    const self = this
    return new ReadableStream<string>({
      async start(controller) {
        try {
          const messages = mapMessages(request.messages)

          const stream = await self.client.chat.completions.create({
            model: request.model,
            messages,
            stream: true,
            ...(request.maxTokens !== undefined ? { max_tokens: request.maxTokens } : {}),
            ...(request.temperature !== undefined ? { temperature: request.temperature } : {}),
          })

          for await (const chunk of stream) {
            const delta = chunk.choices[0]?.delta?.content
            if (delta) controller.enqueue(delta)
          }
          controller.close()
        } catch (err) {
          controller.error(normalizeError(err, self.provider))
        }
      },
    })
  }
}
