/**
 * Universal LLM Adapter
 * Adds reliability (retry, circuit breaker) and observability (automatic token tracking) to ALL LLM calls
 */

import type { LLMAdapter, LLMGenerateRequest, LLMGenerateResponse } from '../types'
import type { LLMModel } from '../model-info'
import type { AIUsageCollector } from '../../../../operations/observability/ai-usage-collector'
import type { AICallContext, BaseAICall } from '../../../../operations/observability/types'
import { executeWithCircuitBreaker } from './circuit-breaker'
import { LLMResponseParseError, LLMInputBlockedError } from '../errors'
import { sanitizeUserInput } from '../input-sanitizer'
import { validateResponseSchema } from '../response-schema-validator'
import { LLM_MAX_ATTEMPTS, LLM_RETRY_DELAYS } from '../../../../platform/constants/resilience'

/**
 * Type guard to check if an object has HTTP status property
 */
interface ErrorWithStatus {
  status: number
}

/**
 * Type guard to check if an object has message property
 */
interface ErrorWithMessage {
  message: string
}

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

/**
 * Type guard for error with message
 */
function hasMessage(error: unknown): error is ErrorWithMessage {
  return (
    typeof error === 'object' &&
    error !== null &&
    'message' in error &&
    typeof (error as ErrorWithMessage).message === 'string'
  )
}

/**
 * Universal wrapper that adds reliability + observability to any LLM adapter
 *
 * Features:
 * - Exponential backoff retry (3 attempts: 1s, 2s, 4s)
 * - Circuit breaker for cascading failure prevention (shared per model)
 * - Automatic token usage tracking with typed contexts
 * - Automatic latency tracking
 *
 * Implements decorator pattern - delegates to base adapter
 *
 * Note: Base adapters may include optional 'usage' field in responses.
 * This wrapper extracts usage for observability and strips it before returning to callers.
 */
export class UniversalLLMAdapter implements LLMAdapter {
  constructor(
    private baseAdapter: LLMAdapter,
    private modelName: LLMModel,
    private aiUsageCollector?: AIUsageCollector,
    private callType?: BaseAICall['callType'],
    private context?: AICallContext
  ) {}

  async generate<T>(request: LLMGenerateRequest): Promise<LLMGenerateResponse<T>> {
    // Sanitize user-role messages BEFORE retry/circuit breaker
    // Blocked input is not transient (no retry) and not a service failure (no circuit breaker impact)
    const sanitizedRequest = this.sanitizeRequest(request)
    return this.withRetryAndTracking(async () => {
      const response = await this.baseAdapter.generate<T>(sanitizedRequest)

      // Validate output against responseSchema (throws retryable LLMResponseParseError)
      if (sanitizedRequest.responseSchema) {
        validateResponseSchema(response.output, sanitizedRequest.responseSchema)
      }

      return response
    })
  }

  /**
   * Sanitize user-role messages for prompt injection attacks
   * System and assistant messages are framework-generated and skipped
   *
   * @throws LLMInputBlockedError if 3+ distinct attack patterns detected
   */
  private sanitizeRequest(request: LLMGenerateRequest): LLMGenerateRequest {
    return {
      ...request,
      messages: request.messages.map((msg) => {
        if (msg.role !== 'user') return msg
        const result = sanitizeUserInput(msg.content)
        if (result.blocked) {
          throw new LLMInputBlockedError(result.warnings)
        }
        return { ...msg, content: result.sanitized }
      })
    }
  }

  /**
   * Wraps LLM call with retry logic, circuit breaker, and observability tracking
   *
   * The circuit breaker wraps the entire retry loop - this ensures that:
   * - Failures during retries are counted toward the circuit breaker threshold
   * - The circuit opens after sustained failures across retry attempts
   * - Success after retries properly records success to the circuit breaker
   */
  private async withRetryAndTracking<T>(fn: () => Promise<LLMGenerateResponse<T>>): Promise<LLMGenerateResponse<T>> {
    // Circuit breaker wraps the entire retry loop
    return executeWithCircuitBreaker(this.modelName, async () => {
      // Retry loop (max 3 attempts with exponential backoff)
      let lastError: unknown

      for (let attempt = 1; attempt <= LLM_MAX_ATTEMPTS; attempt++) {
        try {
          // Measure per-attempt latency (not cumulative across retries)
          const startTime = Date.now()

          // Execute LLM call
          const response = await fn()

          // Calculate latency for this attempt only
          const latencyMs = Date.now() - startTime

          // Extract usage from response (if available)
          if (response.usage && this.aiUsageCollector) {
            this.aiUsageCollector.record(
              {
                model: this.modelName,
                inputTokens: response.usage.inputTokens,
                outputTokens: response.usage.outputTokens,
                latencyMs,
                cost: response.cost // Pass actual cost from provider (e.g., OpenRouter)
              },
              this.callType,
              this.context
            )
          }

          // Return response (usage hidden from callers by TypeScript interface)
          return response
        } catch (error) {
          lastError = error

          // Determine if error is retryable
          const isRetryable = this.isRetryableError(error)

          if (!isRetryable) {
            // Non-retryable error (401, 400, etc.) - fail immediately
            // Let the circuit breaker record this as a failure
            throw error
          }

          // Retryable error (429, 500, 503, network) - retry with exponential backoff
          if (attempt < LLM_MAX_ATTEMPTS) {
            const delay = LLM_RETRY_DELAYS[attempt - 1]
            await this.sleep(delay)
          }
          // If this was the last attempt, the loop ends and we throw below
        }
      }

      // All retries failed - throw to let circuit breaker record failure
      throw lastError
    })
  }

  /**
   * Determine if error should be retried
   */
  private isRetryableError(error: unknown): boolean {
    if (!error || typeof error !== 'object') return false

    // LLMResponseParseError is always retryable (transient JSON parse failures)
    if (error instanceof LLMResponseParseError) {
      return true
    }

    // Check for HTTP status codes using type guard
    if (hasStatus(error)) {
      const status = error.status

      // Retryable: Rate limits, server errors, service unavailable
      if (status === 429 || status === 500 || status === 503) {
        return true
      }

      // Non-retryable: Auth errors, bad requests
      if (status === 401 || status === 400 || status === 403) {
        return false
      }
    }

    // Check for network errors using type guard
    if (hasMessage(error)) {
      const message = error.message
      if (
        message.includes('ECONNRESET') ||
        message.includes('ETIMEDOUT') ||
        message.includes('ENOTFOUND') ||
        message.includes('network')
      ) {
        return true
      }
    }

    // Default: don't retry unknown errors
    return false
  }

  /**
   * Sleep utility for retry delays
   */
  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms))
  }
}
