/**
 * Universal OpenAI-Compatible API Adapter
 *
 * Translates between the Anthropic SDK interface (used throughout SANDCODE)
 * and the OpenAI chat completions format. Works with OpenAI, DeepSeek,
 * Mistral, Gemini (compat endpoint), z.ai, Ollama, Together, Groq, etc.
 *
 * All providers that speak the OpenAI chat completions format can use this
 * adapter — provider differences are captured in the config object.
 */

import type Anthropic from '@sandcode/sdk'
import { APIError } from '@sandcode/sdk'
import OpenAI from 'openai'
import { logForDebugging } from '../../utils/debug.js'

// ─────────────────────────────────────────────────────────────
// Provider config
// ─────────────────────────────────────────────────────────────

export interface OpenAICompatProviderConfig {
  /** Provider name for logging (e.g., 'openai', 'deepseek') */
  name: string
  /** API base URL (e.g., 'https://api.openai.com/v1') */
  baseUrl: string
  /** Bearer token for Authorization header */
  apiKey: string
  /** Default model ID (e.g., 'gpt-4o') */
  defaultModel: string
  /** Whether the model returns reasoning_content (DeepSeek, z.ai) */
  hasReasoningContent?: boolean
  /** Parameter name for max output tokens ('max_tokens' or 'max_completion_tokens') */
  maxTokensParam?: 'max_tokens' | 'max_completion_tokens'
  /** Extra headers (e.g., OpenRouter needs HTTP-Referer) */
  extraHeaders?: Record<string, string>
  /** Whether the provider supports tool use (default: true) */
  supportsTools?: boolean
  /** If true, provider always includes usage in the final streaming chunk
   *  without needing stream_options (e.g., z.ai) */
  alwaysIncludesUsage?: boolean
  /** If true, send tool_stream: true for incremental tool call streaming (z.ai) */
  supportsToolStream?: boolean
  /** If true, send thinking config and preserve reasoning_content in multi-turn (z.ai, DeepSeek) */
  preserveThinking?: boolean
}

// ─────────────────────────────────────────────────────────────
// OpenAI types
// ─────────────────────────────────────────────────────────────

type OAIContentPart =
  | { type: 'text'; text: string }
  | { type: 'image_url'; image_url: { url: string } }

interface OAIChatMessage {
  role: 'system' | 'user' | 'assistant' | 'tool'
  content: string | OAIContentPart[] | null
  tool_calls?: OAIToolCall[]
  tool_call_id?: string
  /** z.ai/DeepSeek: preserved reasoning from previous turns */
  reasoning_content?: string
}

interface OAIToolCall {
  id: string
  type: 'function'
  function: {
    name: string
    arguments: string
  }
}

interface OAIChatCompletionRequest {
  model: string
  messages: OAIChatMessage[]
  stream?: boolean
  stream_options?: { include_usage: boolean }
  temperature?: number
  top_p?: number
  max_tokens?: number
  max_completion_tokens?: number
  tools?: Array<{
    type: 'function'
    function: {
      name: string
      description: string
      parameters: Record<string, unknown>
    }
  }>
  tool_choice?: string
  stop?: string[]
  /** z.ai: enable incremental tool call streaming */
  tool_stream?: boolean
  /** z.ai/DeepSeek: thinking mode configuration */
  thinking?: { type: 'enabled' | 'disabled'; clear_thinking?: boolean }
}

interface OAIChatCompletionResponse {
  id: string
  model: string
  choices: Array<{
    index: number
    message: {
      role: 'assistant'
      content: string | null
      reasoning_content?: string
      tool_calls?: OAIToolCall[]
    }
    finish_reason: string
  }>
  usage: {
    prompt_tokens: number
    completion_tokens: number
    total_tokens: number
    prompt_tokens_details?: {
      cached_tokens?: number
    }
  }
}

// ─────────────────────────────────────────────────────────────
// Stream wrapper — must match @sandcode/sdk Stream<T> shape
// ─────────────────────────────────────────────────────────────

/**
 * Wraps an AsyncGenerator to match the SDK's Stream interface.
 * Critical properties:
 * - `.controller` (AbortController) — used by claude.ts:2906 for cancellation
 * - `[Symbol.asyncIterator]` — used by `for await (const part of stream)`
 * - `'controller' in stream` — used by claude.ts:1853 to distinguish from errors
 */
class AnthropicCompatStream<T> implements AsyncIterable<T> {
  controller: AbortController
  private iterator: AsyncIterator<T>

  constructor(generator: AsyncGenerator<T>, controller: AbortController) {
    this.controller = controller
    this.iterator = generator[Symbol.asyncIterator]()
  }

  [Symbol.asyncIterator](): AsyncIterator<T> {
    return this.iterator
  }
}

// ─────────────────────────────────────────────────────────────
// Message conversion: Anthropic SDK → OpenAI format
// ─────────────────────────────────────────────────────────────

function convertMessagesToOAI(
  system: string | Anthropic.Messages.TextBlockParam[] | undefined,
  messages: Anthropic.Messages.MessageParam[],
  config: OpenAICompatProviderConfig,
): OAIChatMessage[] {
  const result: OAIChatMessage[] = []

  // System message
  if (system) {
    const systemText = typeof system === 'string' ? system : system.map((b) => b.text).join('\n')
    result.push({ role: 'system', content: systemText })
  }

  for (const msg of messages) {
    if (msg.role === 'user') {
      const blocks = Array.isArray(msg.content)
        ? msg.content
        : [{ type: 'text' as const, text: String(msg.content) }]

      // Separate tool_result blocks (they become {role: "tool"} messages)
      // and text/image blocks (they become a single {role: "user"} message)
      const contentParts: OAIContentPart[] = []
      for (const block of blocks) {
        if (block.type === 'tool_result') {
          const content =
            typeof block.content === 'string'
              ? block.content
              : Array.isArray(block.content)
                ? block.content.map((c) => ('text' in c ? c.text : '')).join('\n')
                : ''
          result.push({
            role: 'tool',
            content,
            tool_call_id: block.tool_use_id,
          })
        } else if (block.type === 'text') {
          contentParts.push({ type: 'text', text: block.text })
        } else if (block.type === 'image' && block.source?.type === 'base64') {
          contentParts.push({
            type: 'image_url',
            image_url: {
              url: `data:${block.source.media_type};base64,${block.source.data}`,
            },
          })
        }
      }

      if (contentParts.length > 0) {
        // Use plain string when there are only text parts (wider compat)
        const onlyText = contentParts.every((p) => p.type === 'text')
        result.push({
          role: 'user',
          content: onlyText
            ? (contentParts as Array<{ type: 'text'; text: string }>).map((p) => p.text).join('\n')
            : contentParts,
        })
      }
    } else if (msg.role === 'assistant') {
      const blocks = Array.isArray(msg.content)
        ? msg.content
        : [{ type: 'text' as const, text: String(msg.content) }]

      const textParts: string[] = []
      const toolCalls: OAIToolCall[] = []
      let reasoningContent: string | undefined

      for (const block of blocks) {
        if (block.type === 'text') {
          textParts.push(block.text)
        } else if (block.type === 'tool_use') {
          toolCalls.push({
            id: block.id,
            type: 'function',
            function: {
              name: block.name,
              arguments: JSON.stringify(block.input),
            },
          })
        } else if ((block as { type: string }).type === 'thinking' && config.preserveThinking) {
          // z.ai/DeepSeek require reasoning_content sent back in multi-turn
          const thinking = (block as { thinking: string }).thinking
          if (thinking) {
            reasoningContent = reasoningContent ? reasoningContent + thinking : thinking
          }
        }
      }

      result.push({
        role: 'assistant',
        content: textParts.length > 0 ? textParts.join('\n') : null,
        ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
        ...(reasoningContent ? { reasoning_content: reasoningContent } : {}),
      })
    }
  }

  return result
}

// ─────────────────────────────────────────────────────────────
// Tool conversion: Anthropic SDK → OpenAI format
// ─────────────────────────────────────────────────────────────

function convertToolsToOAI(tools?: Anthropic.Messages.Tool[]): OAIChatCompletionRequest['tools'] {
  if (!tools?.length) return undefined
  return tools.map((tool) => ({
    type: 'function' as const,
    function: {
      name: tool.name,
      description: tool.description ?? '',
      parameters: (tool.input_schema ?? {}) as Record<string, unknown>,
    },
  }))
}

// ─────────────────────────────────────────────────────────────
// Response conversion: OpenAI → Anthropic SDK format
// ─────────────────────────────────────────────────────────────

function convertResponseToAnthropic(
  response: OAIChatCompletionResponse,
  model: string,
  config: OpenAICompatProviderConfig,
): Anthropic.Messages.Message {
  const choice = response.choices[0]
  if (!choice) {
    throw new Error(`${config.name} returned no choices`)
  }

  const contentBlocks: Anthropic.Messages.ContentBlock[] = []

  // Add reasoning/thinking if present
  if (config.hasReasoningContent && choice.message.reasoning_content) {
    contentBlocks.push({
      type: 'thinking',
      thinking: choice.message.reasoning_content,
    } as unknown as Anthropic.Messages.ContentBlock)
  }

  // Add tool calls
  if (choice.message.tool_calls?.length) {
    for (const tc of choice.message.tool_calls) {
      let input: unknown = {}
      try {
        input = JSON.parse(tc.function.arguments)
      } catch {
        input = {}
      }
      contentBlocks.push({
        type: 'tool_use',
        id: tc.id,
        name: tc.function.name,
        input,
      } as Anthropic.Messages.ContentBlock)
    }
  }

  // Add text content
  if (choice.message.content) {
    contentBlocks.push({
      type: 'text',
      text: choice.message.content,
    } as Anthropic.Messages.ContentBlock)
  }

  const stopReasonMap: Record<string, Anthropic.Messages.Message['stop_reason']> = {
    stop: 'end_turn',
    tool_calls: 'tool_use',
    length: 'max_tokens',
  }

  const promptTokens = response.usage?.prompt_tokens ?? 0
  const cachedTokens = response.usage?.prompt_tokens_details?.cached_tokens ?? 0

  return {
    id: response.id || `msg_${config.name}_${Date.now()}`,
    type: 'message',
    role: 'assistant',
    content:
      contentBlocks.length > 0
        ? contentBlocks
        : [{ type: 'text', text: '' } as Anthropic.Messages.ContentBlock],
    model: response.model || model,
    stop_reason: stopReasonMap[choice.finish_reason] ?? 'end_turn',
    stop_sequence: null,
    usage: {
      input_tokens: promptTokens - cachedTokens,
      output_tokens: response.usage?.completion_tokens ?? 0,
      cache_creation_input_tokens: 0,
      cache_read_input_tokens: cachedTokens,
    },
  }
}

// ─────────────────────────────────────────────────────────────
// Streaming adapter: OpenAI SSE → Anthropic SDK events
// ─────────────────────────────────────────────────────────────

/**
 * Convert native OpenAI SDK stream into Anthropic-compatible events.
 * Uses the OpenAI SDK's optimized SSE parser instead of hand-rolled fetch+TextDecoder.
 */
async function* createStreamFromOpenAISDK(
  stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>,
  model: string,
  config: OpenAICompatProviderConfig,
  abortController: AbortController,
): AsyncGenerator<Anthropic.Messages.RawMessageStreamEvent> {
  // Emit message_start
  yield {
    type: 'message_start',
    message: {
      id: `msg_${config.name}_${Date.now()}`,
      type: 'message',
      role: 'assistant',
      content: [],
      model,
      stop_reason: null,
      stop_sequence: null,
      usage: { input_tokens: 0, output_tokens: 0 },
    },
  } as unknown as Anthropic.Messages.RawMessageStreamEvent

  let blockIndex = 0
  let textBlockStarted = false
  const activeToolCalls = new Map<
    number,
    { id: string; name: string; blockIdx: number; args: string }
  >()
  let reasoningBlockStarted = false

  let streamInputTokens = 0
  let streamOutputTokens = 0
  let streamCachedTokens = 0
  let lastStopReason: string = 'end_turn'

  for await (const chunk of stream) {
    if (abortController.signal.aborted) break

    // Capture usage whenever we see it
    if (chunk.usage) {
      if (chunk.usage.prompt_tokens) streamInputTokens = chunk.usage.prompt_tokens
      if (chunk.usage.completion_tokens) streamOutputTokens = chunk.usage.completion_tokens
      if ((chunk.usage as Record<string, unknown>).prompt_tokens_details) {
        const details = (chunk.usage as Record<string, unknown>).prompt_tokens_details as {
          cached_tokens?: number
        }
        if (details?.cached_tokens) streamCachedTokens = details.cached_tokens
      }
    }

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

    const delta = choice.delta as Record<string, unknown>

    // Handle reasoning_content (DeepSeek, z.ai)
    if (config.hasReasoningContent && delta.reasoning_content) {
      if (!reasoningBlockStarted) {
        yield {
          type: 'content_block_start',
          index: blockIndex,
          content_block: { type: 'thinking', thinking: '' },
        } as unknown as Anthropic.Messages.RawMessageStreamEvent
        reasoningBlockStarted = true
      }
      yield {
        type: 'content_block_delta',
        index: blockIndex,
        delta: { type: 'thinking_delta', thinking: delta.reasoning_content },
      } as unknown as Anthropic.Messages.RawMessageStreamEvent
    }

    // Handle tool calls
    if (choice.delta.tool_calls) {
      if (reasoningBlockStarted) {
        yield {
          type: 'content_block_stop',
          index: blockIndex,
        } as unknown as Anthropic.Messages.RawMessageStreamEvent
        blockIndex++
        reasoningBlockStarted = false
      }
      if (textBlockStarted) {
        yield {
          type: 'content_block_stop',
          index: blockIndex,
        } as unknown as Anthropic.Messages.RawMessageStreamEvent
        blockIndex++
        textBlockStarted = false
      }

      for (const tc of choice.delta.tool_calls) {
        const existing = activeToolCalls.get(tc.index)

        if (!existing && tc.id && tc.function?.name) {
          const toolBlockIdx = blockIndex
          activeToolCalls.set(tc.index, {
            id: tc.id,
            name: tc.function.name,
            blockIdx: toolBlockIdx,
            args: tc.function.arguments ?? '',
          })
          yield {
            type: 'content_block_start',
            index: toolBlockIdx,
            content_block: { type: 'tool_use', id: tc.id, name: tc.function.name, input: {} },
          } as unknown as Anthropic.Messages.RawMessageStreamEvent

          if (tc.function.arguments) {
            yield {
              type: 'content_block_delta',
              index: toolBlockIdx,
              delta: { type: 'input_json_delta', partial_json: tc.function.arguments },
            } as unknown as Anthropic.Messages.RawMessageStreamEvent
          }
          blockIndex++
        } else if (existing && tc.function?.arguments) {
          existing.args += tc.function.arguments
          yield {
            type: 'content_block_delta',
            index: existing.blockIdx,
            delta: { type: 'input_json_delta', partial_json: tc.function.arguments },
          } as unknown as Anthropic.Messages.RawMessageStreamEvent
        }
      }
    }

    // Handle text content
    if (choice.delta.content) {
      if (reasoningBlockStarted) {
        yield {
          type: 'content_block_stop',
          index: blockIndex,
        } as unknown as Anthropic.Messages.RawMessageStreamEvent
        blockIndex++
        reasoningBlockStarted = false
      }
      if (!textBlockStarted) {
        yield {
          type: 'content_block_start',
          index: blockIndex,
          content_block: { type: 'text', text: '' },
        } as unknown as Anthropic.Messages.RawMessageStreamEvent
        textBlockStarted = true
      }
      yield {
        type: 'content_block_delta',
        index: blockIndex,
        delta: { type: 'text_delta', text: choice.delta.content },
      } as unknown as Anthropic.Messages.RawMessageStreamEvent
    }

    // Handle finish
    if (choice.finish_reason) {
      if (reasoningBlockStarted) {
        yield {
          type: 'content_block_stop',
          index: blockIndex,
        } as unknown as Anthropic.Messages.RawMessageStreamEvent
        blockIndex++
        reasoningBlockStarted = false
      }
      for (const [, tc] of activeToolCalls) {
        yield {
          type: 'content_block_stop',
          index: tc.blockIdx,
        } as unknown as Anthropic.Messages.RawMessageStreamEvent
      }
      activeToolCalls.clear()
      if (textBlockStarted) {
        yield {
          type: 'content_block_stop',
          index: blockIndex,
        } as unknown as Anthropic.Messages.RawMessageStreamEvent
        textBlockStarted = false
      }
      lastStopReason =
        choice.finish_reason === 'tool_calls'
          ? 'tool_use'
          : choice.finish_reason === 'length'
            ? 'max_tokens'
            : 'end_turn'
    }
  }

  logForDebugging(
    `[${config.name}] stream done: stop=${lastStopReason}, input=${streamInputTokens}, output=${streamOutputTokens}, cached=${streamCachedTokens}`,
  )
  yield {
    type: 'message_delta',
    delta: { stop_reason: lastStopReason },
    usage: {
      input_tokens: streamInputTokens - streamCachedTokens,
      output_tokens: streamOutputTokens,
      cache_read_input_tokens: streamCachedTokens,
    },
  } as unknown as Anthropic.Messages.RawMessageStreamEvent

  yield { type: 'message_stop' } as unknown as Anthropic.Messages.RawMessageStreamEvent
}

// ─────────────────────────────────────────────────────────────
// Client factory
// ─────────────────────────────────────────────────────────────

/**
 * Create an Anthropic-SDK-compatible client backed by an OpenAI-format API.
 * Works with any provider that speaks the OpenAI chat completions protocol.
 */
export function createOpenAICompatClient(config: OpenAICompatProviderConfig): Anthropic {
  logForDebugging(`[${config.name}] Creating OpenAI-compat client: ${config.baseUrl}`)

  // Create a shared OpenAI SDK client instance (reuses connections)
  const openaiClient = new OpenAI({
    apiKey: config.apiKey,
    baseURL: config.baseUrl,
    defaultHeaders: config.extraHeaders,
    dangerouslyAllowBrowser: true,
  })

  async function doCreate(params: Record<string, unknown>, _options?: Record<string, unknown>) {
    const abortController = new AbortController()

    const modelId = (params.model as string) || config.defaultModel
    const isStreaming = params.stream === true

    // Build messages in OpenAI format
    const messages = convertMessagesToOAI(
      params.system as string | Anthropic.Messages.TextBlockParam[] | undefined,
      params.messages as Anthropic.Messages.MessageParam[],
      config,
    )

    // Build tools in OpenAI format
    const tools =
      params.tools && config.supportsTools !== false
        ? convertToolsToOAI(params.tools as Anthropic.Messages.Tool[])
        : undefined

    const maxTokensKey = config.maxTokensParam ?? 'max_tokens'

    // Build base request params for OpenAI SDK
    const requestBody: Record<string, unknown> = {
      model: modelId,
      messages,
      [maxTokensKey]: (params.max_tokens as number) ?? 8192,
      ...(params.temperature !== undefined ? { temperature: params.temperature as number } : {}),
      ...(tools ? { tools, tool_choice: 'auto' as const } : {}),
      // z.ai: enable incremental tool call streaming
      ...(config.supportsToolStream && isStreaming && params.tools ? { tool_stream: true } : {}),
      // z.ai/DeepSeek thinking-mode. Honor caller-provided `params.thinking`
      // (ThinkingToggle UI sets this) — fall back to enabled+preserved when
      // provider defaults to thinking.
      ...(() => {
        const incoming = params.thinking as
          | { type?: 'enabled' | 'disabled'; clear_thinking?: boolean }
          | undefined
        if (incoming?.type === 'disabled') {
          return { thinking: { type: 'disabled' as const } }
        }
        if (config.preserveThinking) {
          return {
            thinking: {
              type: 'enabled' as const,
              clear_thinking: incoming?.clear_thinking ?? false,
            },
          }
        }
        return {}
      })(),
    }

    // Request usage in streaming — unless provider already includes it (e.g., z.ai)
    if (isStreaming && !config.alwaysIncludesUsage) {
      requestBody.stream_options = { include_usage: true }
    }

    logForDebugging(
      `[${config.name}] POST ${config.baseUrl}/chat/completions model=${modelId} stream=${isStreaming}`,
    )

    // Pass the signal from options if present (used for cancellation)
    const fetchSignal = (_options as { signal?: AbortSignal } | undefined)?.signal

    try {
      if (isStreaming) {
        // Use native OpenAI SDK streaming — optimized SSE parser
        const stream = await openaiClient.chat.completions.create(
          { ...requestBody, stream: true } as Parameters<
            typeof openaiClient.chat.completions.create
          >[0],
          { signal: fetchSignal ?? abortController.signal },
        )
        const generator = createStreamFromOpenAISDK(stream, modelId, config, abortController)
        return new AnthropicCompatStream(generator, abortController)
      }

      // Non-streaming: use OpenAI SDK directly
      const response = await openaiClient.chat.completions.create(
        requestBody as Parameters<typeof openaiClient.chat.completions.create>[0],
        { signal: fetchSignal ?? abortController.signal },
      )
      return convertResponseToAnthropic(
        response as unknown as OAIChatCompletionResponse,
        modelId,
        config,
      )
    } catch (err) {
      if (err instanceof OpenAI.APIError) {
        throw new APIError(
          err.status,
          err.error as object | undefined,
          `${config.name} API error (${err.status}): ${err.message}`,
          err.headers as unknown as Record<string, string>,
        )
      }
      const message = err instanceof Error ? err.message : String(err)
      throw new APIError(
        undefined,
        undefined,
        `${config.name} connection error: ${message}`,
        undefined,
      )
    }
  }

  // Build the client proxy matching the Anthropic SDK interface
  const client = {
    beta: {
      messages: {
        create: (params: Record<string, unknown>, options?: Record<string, unknown>) => {
          // Must return a Promise with .withResponse() attached
          // claude.ts:1832 calls .create({stream:true}).withResponse()
          let httpResponse: Response | undefined
          const promise = doCreate(params, options).then((result) => {
            return result
          })

          return Object.assign(promise, {
            withResponse: async () => {
              const data = await promise
              return {
                data,
                response: httpResponse ?? new Response(),
                request_id: `${config.name}_${Date.now()}`,
              }
            },
          })
        },
      },
    },
    messages: {
      create: (params: Record<string, unknown>, options?: Record<string, unknown>) => {
        return client.beta.messages.create(params, options)
      },
    },
  }

  return client as unknown as Anthropic
}
