/**
 * Generic OpenAI Adapter
 * Stateless translation layer between universal protocol and OpenAI API
 *
 * Responsibilities:
 * 1. Translate LLMGenerateRequest to OpenAI format
 * 2. Call OpenAI API with structured output
 * 3. Parse OpenAI response
 * 4. Translate back to LLMGenerateResponse
 */

import type OpenAI from 'openai'
import type { LLMAdapter, LLMGenerateRequest, LLMGenerateResponse } from '../../types'
import type { LLMModel, ModelSpecificOptions } from '../../model-info'
import { getModelInfo } from '../../model-info'
import { TokenLimitExceededError, LLMResponseParseError } from '../../errors'
import { bslogExternalServiceError, isSystemError } from '../../../../../platform/utils/server/betterstack-logger'
import { composeSignal } from './compose-signal'
import { DEFAULT_LLM_TIMEOUT } from '../../../../../platform/constants/timeouts'

/**
 * Type guard for errors with HTTP status code (e.g., OpenAI API errors)
 */
interface ErrorWithStatus {
  status: number
}

function hasStatus(error: unknown): error is ErrorWithStatus {
  return (
    typeof error === 'object' &&
    error !== null &&
    'status' in error &&
    typeof (error as ErrorWithStatus).status === 'number'
  )
}

/**
 * OpenAI adapter configuration
 */
export interface OpenAIAdapterConfig {
  apiKey: string
  model: LLMModel
  modelOptions?: ModelSpecificOptions
}

/**
 * Check if the model and provider combination is for OpenAI
 *
 * @param model - Model identifier (e.g., 'gpt-5-mini', 'gpt-4')
 * @param provider - Provider name
 * @returns True if this is an OpenAI model configuration
 */
export function isOpenAIModel(model: LLMModel, provider: string): boolean {
  return model.startsWith('gpt-') && provider === 'openai'
}

/**
 * Generic OpenAI Adapter - Implements universal protocol
 * Uses structured output for guaranteed schema compliance
 */
export class OpenAIAdapter implements LLMAdapter {
  private clientPromise: Promise<OpenAI>
  private model: LLMModel
  private modelOptions?: ModelSpecificOptions

  constructor(config: OpenAIAdapterConfig) {
    // Lazy load OpenAI SDK
    this.clientPromise = import('openai').then(
      ({ default: OpenAIClass }) =>
        new OpenAIClass({
          apiKey: config.apiKey
        })
    )
    this.model = config.model || 'gpt-5.4-mini'
    this.modelOptions = config.modelOptions
  }

  /**
   * Generate structured output from messages using OpenAI
   *
   * @param request - Generic generation request
   * @returns Structured output with usage metadata
   */
  async generate<T>(request: LLMGenerateRequest): Promise<LLMGenerateResponse<T>> {
    const client = await this.clientPromise

    try {
      const createParams: OpenAI.Chat.ChatCompletionCreateParams = {
        model: this.model,
        messages: request.messages.map((msg) => ({
          role: msg.role,
          content: msg.content
        })),
        max_completion_tokens: request.maxOutputTokens,
        response_format: {
          type: 'json_schema',
          json_schema: {
            name: 'llm_response',
            schema: request.responseSchema as Record<string, unknown>
          }
        }
      }

      // Only add temperature if specified and not default (1)
      if (request.temperature !== undefined && request.temperature !== 1) {
        createParams.temperature = request.temperature
      }

      // Only add topP if specified
      if (request.topP !== undefined) {
        createParams.top_p = request.topP
      }

      // Apply model-specific options if present (type narrowing for union)
      if (this.modelOptions && 'reasoning_effort' in this.modelOptions) {
        if (this.modelOptions.reasoning_effort) {
          createParams.reasoning_effort = this.modelOptions.reasoning_effort
        }
      }

      if (this.modelOptions && 'verbosity' in this.modelOptions) {
        if (this.modelOptions.verbosity) {
          createParams.verbosity = this.modelOptions.verbosity
        }
      }

      const response = await client.chat.completions.create(createParams, {
        signal: composeSignal(DEFAULT_LLM_TIMEOUT, request.signal)
      })

      const choice = response.choices[0]
      if (!choice) {
        throw new Error('No response from OpenAI')
      }

      // DETECT TOKEN LIMIT ERROR - OpenAI-specific
      if (choice.finish_reason === 'length') {
        const usage = response.usage
        const reasoningTokens = usage?.completion_tokens_details?.reasoning_tokens || 0
        const completionTokens = usage?.completion_tokens || 0
        const modelInfo = getModelInfo(this.model)

        throw new TokenLimitExceededError(
          `LLM token limit exceeded during generation. ` +
            `Model consumed all available tokens during reasoning phase with no tokens left for output.`,
          {
            model: this.model,
            maxOutputTokens: request.maxOutputTokens,
            usedTokens: completionTokens,
            reasoningTokens,
            outputTokens: completionTokens - reasoningTokens,
            recommendation: modelInfo
              ? `Increase maxOutputTokens to at least ${modelInfo.minTokens} for this model.`
              : 'Increase maxOutputTokens for this model.'
          }
        )
      }

      // Check for empty content (secondary validation)
      const content = choice.message.content || '{}'

      // Parse JSON with error wrapping for retry support
      let output: unknown
      try {
        output = JSON.parse(content)
      } catch (error) {
        if (error instanceof SyntaxError) {
          throw new LLMResponseParseError(`Failed to parse LLM response as JSON: ${error.message}`, {
            rawContent: content.substring(0, 500),
            parseError: error.message
          })
        }
        throw error
      }

      // Extract usage data from OpenAI response (for wrapper to track)
      const usage = response.usage
        ? {
            inputTokens: response.usage.prompt_tokens,
            outputTokens: response.usage.completion_tokens,
            totalTokens: response.usage.total_tokens
          }
        : undefined

      // Return response with usage (wrapper will extract and strip it)
      return {
        output: output as T,
        usage
      }
    } catch (error) {
      // Add Better Stack logging for system errors (5xx only)
      if (isSystemError(error)) {
        bslogExternalServiceError('openai', 'chat.completions.create', error as Error, {
          model: this.model,
          statusCode: hasStatus(error) ? error.status : undefined,
          reasoningEffort:
            this.modelOptions && 'reasoning_effort' in this.modelOptions
              ? this.modelOptions.reasoning_effort
              : undefined
        })
      }

      throw error
    }
  }
}
