/**
 * OpenAICompatibleAdapter - Provider adapter for OpenAI-compatible APIs
 *
 * Implements ProviderAdapter; uses AnthropicCanonicalConverter.
 *
 * @module packages/llm/src/adapters/OpenAICompatibleAdapter
 */

import type { CanonicalMessage } from '../canonical/AnthropicCanonical.js'
import { AnthropicCanonicalConverter } from '../canonical/AnthropicCanonical.js'

// Local type definitions (contract types not yet finalized)
type CompleteOptions = { max_tokens?: number; temperature?: number; top_p?: number; system?: string }
type StreamEvent = { type: 'text' | 'thinking' | 'done'; content?: string; reason?: string }

export interface OpenAICompatibleConfig {
  api_key?: string
  base_url: string
  model: string
  max_retries?: number
  timeout?: number
}

export class OpenAICompatibleAdapter {
  private api_key: string
  private base_url: string
  private model: string
  private max_retries: number
  private timeout: number
  private converter: AnthropicCanonicalConverter

  constructor(config: OpenAICompatibleConfig) {
    this.api_key = config.api_key || process.env.OPENAI_API_KEY || 'dummy'
    this.base_url = config.base_url
    this.model = config.model
    this.max_retries = config.max_retries || 3
    this.timeout = config.timeout || 60000
    this.converter = new AnthropicCanonicalConverter()
  }

  async list_models(): Promise<string[]> {
    // Try to fetch model list, fallback to default
    try {
      const response = await fetch(`${this.base_url}/v1/models`, {
        headers: { Authorization: `Bearer ${this.api_key}` }
      })
      if (response.ok) {
        const data = await response.json() as { data: Array<{ id: string }> }
        return data.data.map(m => m.id)
      }
    } catch {
      // Ignore
    }
    return [this.model]
  }

  async validate_model(model: string): Promise<{ valid: boolean; error?: string }> {
    const known = await this.list_models()
    if (known.includes(model)) {
      return { valid: true }
    }
    // Allow unknown models - might be valid
    return { valid: true }
  }

  async complete(
    messages: CanonicalMessage[],
    _requirement: { model: string },
    options: CompleteOptions = {}
  ): Promise<{ content: string; usage?: { input_tokens: number; output_tokens: number } }> {
    // Convert to OpenAI format
    const openai_messages = messages.map(m => ({
      role: m.role,
      content: m.content.map(c => {
        if (c.type === 'text') return { type: 'text', text: c.text }
        if (c.type === 'tool_use') return { type: 'tool_use', id: c.id, name: c.name, input: c.input }
        return { type: 'text', text: '' }
      })
    }))

    const response = await this.make_request({
      model: this.model,
      messages: openai_messages,
      max_tokens: options.max_tokens || 4096,
      temperature: options.temperature,
      top_p: options.top_p,
      stream: false
    })

    const content = (response.choices?.[0]?.message?.content as string) || ''
    const usage = response.usage ? { input_tokens: response.usage.prompt_tokens, output_tokens: response.usage.completion_tokens } : undefined

    return { content, usage }
  }

  async *stream_complete(
    messages: CanonicalMessage[],
    _requirement: { model: string },
    options: CompleteOptions = {}
  ): AsyncGenerator<StreamEvent> {
    const openai_messages = messages.map(m => ({
      role: m.role,
      content: m.content.map(c => {
        if (c.type === 'text') return { type: 'text', text: c.text }
        return { type: 'text', text: '' }
      })
    }))

    const response = await this.make_request({
      model: this.model,
      messages: openai_messages,
      max_tokens: options.max_tokens || 4096,
      temperature: options.temperature,
      top_p: options.top_p,
      stream: true
    })

    const reader = response.body?.getReader()
    if (!reader) {
      throw new Error('No response body')
    }

    const decoder = new TextDecoder()
    let buffer = ''

    while (true) {
      const { done, value } = await reader.read()
      if (done) break

      buffer += decoder.decode(value, { stream: true })
      const lines = buffer.split('\n')
      buffer = lines.pop() || ''

      for (const line of lines) {
        if (!line.trim() || !line.startsWith('data: ')) continue

        const data = line.slice(6)
        if (data === '[DONE]') {
          yield { type: 'done', reason: 'stop' }
          return
        }

        try {
          const event = JSON.parse(data)
          const choice = event.choices?.[0]
          if (!choice) continue

          if (choice.delta?.content) {
            yield { type: 'text', content: choice.delta.content }
          }
          if (choice.finish_reason) {
            yield { type: 'done', reason: choice.finish_reason }
          }
        } catch {
          // Skip
        }
      }
    }
  }

  async count_tokens(text: string): Promise<number> {
    // Simple estimation
    return Math.ceil(text.length / 4)
  }

  private async make_request(body: Record<string, unknown>): Promise<{ ok: boolean; status: number; body?: { getReader(): { read(): Promise<{ done: boolean; value: Uint8Array }> }; choices?: Array<{ message?: { content: string }; delta?: { content: string }; finish_reason?: string }>; usage?: { prompt_tokens: number; completion_tokens: number } } }> {
    const response = await fetch(`${this.base_url}/v1/chat/completions`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${this.api_key}`
      },
      body: JSON.stringify(body)
    })

    if (!response.ok) {
      const error = await response.text()
      throw new Error(`OpenAI-compatible API error: ${response.status} - ${error}`)
    }

    // Handle streaming vs non-streaming
    const is_streaming = body.stream === true
    if (is_streaming) {
      return { ok: true, status: 200, body: response.body as any }
    }

    return { ok: true, status: 200, body: await response.json() as any }
  }
}

export function createOpenAICompatibleAdapter(config: OpenAICompatibleConfig): OpenAICompatibleAdapter {
  return new OpenAICompatibleAdapter(config)
}