/**
 * OpenRouter Adapter
 * Stateless translation layer between universal protocol and OpenRouter API
 *
 * OpenRouter provides access to 400+ LLM models (Claude, Gemini, Llama, etc.)
 * via an OpenAI-compatible API with a single API key.
 *
 * Responsibilities:
 * 1. Translate LLMGenerateRequest to OpenRouter format
 * 2. Call OpenRouter API with structured output
 * 3. Parse OpenRouter response
 * 4. Translate back to LLMGenerateResponse
 */

import type OpenAI from 'openai'
import type { LLMAdapter, LLMGenerateRequest, LLMGenerateResponse } from '../../types'
import type { LLMModel, OpenRouterOptions } 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
 */
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'
  )
}

/**
 * OpenRouter-specific extra_body options for usage accounting and routing
 */
interface OpenRouterExtraBody {
  usage?: { include: boolean }
  transforms?: string[]
  route?: 'fallback'
}

/**
 * Extended params type for OpenRouter with extra_body support
 */
type OpenRouterCreateParams = OpenAI.Chat.ChatCompletionCreateParamsNonStreaming & {
  extra_body?: OpenRouterExtraBody
}

/**
 * OpenRouter adapter configuration
 */
export interface OpenRouterAdapterConfig {
  apiKey: string
  model: LLMModel
  modelOptions?: OpenRouterOptions
}

/**
 * Check if the model is an OpenRouter model
 *
 * @param model - Model identifier (e.g., 'openrouter/anthropic/claude-3.5-sonnet')
 * @returns True if this is an OpenRouter model
 */
export function isOpenRouterModel(model: LLMModel): boolean {
  return typeof model === 'string' && model.startsWith('openrouter/')
}

/**
 * OpenRouter Adapter - Implements universal protocol
 * Uses OpenAI SDK with custom baseURL for OpenRouter API
 */
export class OpenRouterAdapter implements LLMAdapter {
  private clientPromise: Promise<OpenAI>
  private model: LLMModel
  private openRouterModel: string
  private modelOptions?: OpenRouterOptions

  constructor(config: OpenRouterAdapterConfig) {
    // Strip 'openrouter/' prefix for API calls
    // e.g., 'openrouter/anthropic/claude-3.5-sonnet' -> 'anthropic/claude-3.5-sonnet'
    this.openRouterModel = config.model.replace('openrouter/', '')
    this.model = config.model
    this.modelOptions = config.modelOptions

    // Lazy load OpenAI SDK with OpenRouter configuration
    this.clientPromise = import('openai').then(
      ({ default: OpenAIClass }) =>
        new OpenAIClass({
          baseURL: 'https://openrouter.ai/api/v1',
          apiKey: config.apiKey,
          defaultHeaders: {
            'HTTP-Referer': 'https://elevasis.com',
            'X-Title': 'Elevasis'
          }
        })
    )
  }

  /**
   * Generate structured output from messages using OpenRouter
   *
   * @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 {
      // Build OpenRouter-specific extra_body with usage accounting
      const extraBody: OpenRouterExtraBody = {
        usage: { include: true }
      }

      if (this.modelOptions?.transforms) {
        extraBody.transforms = this.modelOptions.transforms
      }

      if (this.modelOptions?.route) {
        extraBody.route = this.modelOptions.route
      }

      const createParams: OpenRouterCreateParams = {
        model: this.openRouterModel,
        messages: request.messages.map((msg) => ({
          role: msg.role,
          content: msg.content
        })),
        max_tokens: request.maxOutputTokens,
        response_format: {
          type: 'json_schema',
          json_schema: {
            name: 'llm_response',
            strict: true,
            schema: request.responseSchema as Record<string, unknown>
          }
        },
        extra_body: extraBody
      }

      // Add temperature if specified
      if (request.temperature !== undefined) {
        createParams.temperature = request.temperature
      }

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

      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 OpenRouter')
      }

      // Detect token limit error
      if (choice.finish_reason === 'length') {
        const usage = response.usage
        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 with no tokens left for output.`,
          {
            model: this.model,
            maxOutputTokens: request.maxOutputTokens,
            usedTokens: completionTokens,
            reasoningTokens: 0, // OpenRouter doesn't expose reasoning tokens
            outputTokens: completionTokens,
            recommendation: modelInfo
              ? `Increase maxOutputTokens to at least ${modelInfo.minTokens} for this model.`
              : 'Increase maxOutputTokens for this model.'
          }
        )
      }

      // Parse response content with error wrapping for retry support
      const content = choice.message.content || '{}'
      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
      const usage = response.usage
        ? {
            inputTokens: response.usage.prompt_tokens,
            outputTokens: response.usage.completion_tokens,
            totalTokens: response.usage.total_tokens
          }
        : undefined

      // Extract actual cost from OpenRouter (when usage accounting is enabled)
      // OpenRouter returns cost in the usage object as an extended field
      const usageWithCost = response.usage as { cost?: number } | undefined
      const cost = usageWithCost?.cost

      return {
        output: output as T,
        usage,
        cost
      }
    } catch (error) {
      // Add Better Stack logging for system errors (5xx only)
      if (isSystemError(error)) {
        bslogExternalServiceError('openrouter', 'chat.completions.create', error as Error, {
          model: this.model,
          openRouterModel: this.openRouterModel,
          statusCode: hasStatus(error) ? error.status : undefined
        })
      }

      throw error
    }
  }
}
