/**
 * Google Gemini Adapter
 * Stateless translation layer between universal protocol and Google GenAI SDK
 *
 * Responsibilities:
 * 1. Translate LLMGenerateRequest to Google GenAI format
 * 2. Transform JSON Schema to Gemini-compatible format (const -> enum, inline $ref)
 * 3. Call Google GenAI SDK with structured output
 * 4. Parse Google response
 * 5. Translate back to LLMGenerateResponse
 */

import type { LLMAdapter, LLMGenerateRequest, LLMGenerateResponse } from '../../types'
import type { LLMModel, GoogleModel, ModelSpecificOptions } from '../../model-info'
import { 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'

// ============================================================================
// Schema Transformation for Gemini Compatibility
// ============================================================================

/**
 * Transform JSON Schema for Gemini API compatibility
 *
 * Gemini uses a subset of JSON Schema. This function handles:
 * 1. const: "value" -> enum: ["value"] + type: "string" (const keyword not supported)
 * 2. Inline $ref references from $defs (for schema reuse)
 * 3. Remove $schema property (Gemini rejects it)
 * 4. Add placeholder property to empty object types (Gemini requires non-empty properties)
 * 5. Ensure objects with additionalProperties also have a properties field
 *
 * @param schema - JSON Schema object to transform
 * @param definitions - Map of $defs for $ref resolution (built during traversal)
 * @returns Gemini-compatible schema
 */
function transformSchemaForGemini(schema: unknown, definitions?: Record<string, unknown>): unknown {
  // Base case: null, undefined, or primitive values pass through unchanged
  if (!schema || typeof schema !== 'object') {
    return schema
  }

  const obj = schema as Record<string, unknown>
  const result: Record<string, unknown> = {}

  // Extract $defs at the current level for reference resolution
  // These definitions are accumulated as we traverse down
  if (obj.$defs && typeof obj.$defs === 'object') {
    definitions = { ...definitions, ...(obj.$defs as Record<string, unknown>) }
  }

  for (const [key, value] of Object.entries(obj)) {
    // Skip $schema - Gemini doesn't accept JSON Schema meta-schema
    if (key === '$schema') {
      continue
    }

    // Skip $defs - definitions are inlined when $ref is encountered
    if (key === '$defs') {
      continue
    }

    // Handle $ref - inline the referenced definition
    // Format: "#/$defs/DefinitionName"
    if (key === '$ref' && typeof value === 'string') {
      const refPath = value.replace('#/$defs/', '')
      if (definitions?.[refPath]) {
        // Return the inlined definition (transformed recursively)
        return transformSchemaForGemini(definitions[refPath], definitions)
      }
      // Skip unresolvable refs - they would cause errors anyway
      continue
    }

    // Transform const to enum - Gemini doesn't support JSON Schema const keyword
    // const: "value" becomes type: "string", enum: ["value"]
    // This handles z.literal() from Zod which generates const
    // Note: Gemini requires type: "string" when enum is present
    if (key === 'const') {
      result.type = 'string'
      result.enum = [value]
      continue
    }

    // Recursively transform nested structures
    if (Array.isArray(value)) {
      // Transform each array element (e.g., anyOf, oneOf, allOf, items as tuple)
      result[key] = value.map((item) => transformSchemaForGemini(item, definitions))
    } else if (value && typeof value === 'object') {
      // Transform nested objects (properties, items, additionalProperties, etc.)
      result[key] = transformSchemaForGemini(value, definitions)
    } else {
      // Primitive values pass through unchanged
      result[key] = value
    }
  }

  // SAFEGUARD: Gemini requires objects to have non-empty properties
  // This handles several cases:
  // 1. Object with empty properties: { type: "object", properties: {} }
  // 2. Object with additionalProperties but no properties: { type: "object", additionalProperties: true }
  // 3. Object with no properties field at all: { type: "object" }
  //
  // Google's API strictly requires properties to be non-empty for OBJECT type,
  // even when additionalProperties is present.
  if (result.type === 'object') {
    const props = result.properties as Record<string, unknown> | undefined

    if (!props || Object.keys(props).length === 0) {
      // Use a property name that:
      // 1. Google accepts (no underscore prefix)
      // 2. Is clearly a placeholder (won't conflict with real data)
      // 3. Is optional so it won't pollute required outputs
      result.properties = {
        placeholderField: {
          type: 'string',
          description: 'System placeholder for empty object schema. Ignore this field.'
        }
      }
    }
  }

  return result
}

/**
 * Google adapter configuration
 */
export interface GoogleAdapterConfig {
  apiKey: string
  model: GoogleModel
  modelOptions?: ModelSpecificOptions
}

/**
 * Check if the model and provider combination is for Google
 *
 * @param model - Model identifier (e.g., 'gemini-3-flash-preview')
 * @param provider - Provider name
 * @returns True if this is a Google model configuration
 */
export function isGoogleModel(model: LLMModel, provider: string): model is GoogleModel {
  return model.startsWith('gemini-') && provider === 'google'
}

/**
 * Google Gemini Adapter - Implements universal protocol
 * Uses structured output via JSON schema for guaranteed schema compliance
 */
export class GoogleAdapter implements LLMAdapter {
  private clientPromise: Promise<import('@google/genai').GoogleGenAI>
  private model: GoogleModel
  private modelOptions?: ModelSpecificOptions

  constructor(config: GoogleAdapterConfig) {
    // Lazy load Google GenAI SDK
    this.clientPromise = import('@google/genai').then(
      ({ GoogleGenAI }) =>
        new GoogleGenAI({
          apiKey: config.apiKey
        })
    )
    this.model = config.model
    this.modelOptions = config.modelOptions
  }

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

    try {
      // Build config object for structured output
      // Transform schema for Gemini compatibility (const -> enum, inline $ref)
      const generateConfig: Record<string, unknown> = {
        responseMimeType: 'application/json',
        responseSchema: transformSchemaForGemini(request.responseSchema)
      }

      // Add thinking level if specified (Gemini 3 specific feature)
      if (this.modelOptions && 'thinkingLevel' in this.modelOptions) {
        generateConfig.thinkingConfig = {
          thinkingLevel: this.modelOptions.thinkingLevel
        }
      }

      // Add max tokens if specified
      if (request.maxOutputTokens) {
        generateConfig.maxOutputTokens = request.maxOutputTokens
      }

      // Convert messages to Gemini format
      // Gemini uses 'model' role instead of 'assistant' for AI messages
      // System messages need special handling - prepend to first user message or use systemInstruction
      const systemMessages = request.messages.filter((msg) => msg.role === 'system')
      const nonSystemMessages = request.messages.filter((msg) => msg.role !== 'system')

      // Use systemInstruction for system messages if present
      if (systemMessages.length > 0) {
        generateConfig.systemInstruction = systemMessages.map((msg) => msg.content).join('\n\n')
      }

      // Convert remaining messages to Gemini content format
      const contents = nonSystemMessages.map((msg) => ({
        role: msg.role === 'assistant' ? 'model' : 'user',
        parts: [{ text: msg.content }]
      }))

      const response = await ai.models.generateContent({
        model: this.model,
        contents,
        config: { ...generateConfig, abortSignal: composeSignal(DEFAULT_LLM_TIMEOUT, request.signal) }
      })

      // Detect empty/blocked responses before parsing
      // Gemini returns no text when: safety filters block output, prompt is blocked,
      // model produces empty candidates, or finish reason is non-STOP
      if (!response.text) {
        const candidate = response.candidates?.[0]
        const finishReason = candidate?.finishReason
        const blockReason = response.promptFeedback?.blockReason

        const reasons: string[] = []
        if (blockReason) reasons.push(`prompt blocked: ${blockReason}`)
        if (finishReason && finishReason !== 'STOP') reasons.push(`finish reason: ${finishReason}`)
        if (!response.candidates?.length) reasons.push('no candidates returned')

        const detail = reasons.length > 0 ? reasons.join(', ') : 'empty response.text with no diagnostic metadata'

        throw new LLMResponseParseError(`Google Gemini returned no output (${detail})`, {
          finishReason,
          blockReason,
          hasCandidates: (response.candidates?.length ?? 0) > 0
        })
      }

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

      // Extract usage metadata for observability tracking
      const usage = response.usageMetadata
        ? {
            inputTokens: response.usageMetadata.promptTokenCount || 0,
            outputTokens: response.usageMetadata.candidatesTokenCount || 0,
            totalTokens: response.usageMetadata.totalTokenCount || 0
          }
        : 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('google-genai', 'models.generateContent', error as Error, {
          model: this.model,
          thinkingLevel:
            this.modelOptions && 'thinkingLevel' in this.modelOptions ? this.modelOptions.thinkingLevel : undefined
        })
      }

      throw error
    }
  }
}
