/**
 * Main TypedFetch Implementation
 */

import { WTinyLFUCache } from '../cache/w-tinylfu.js'
import { RequestDeduplicator } from '../cache/deduplicator.js'
import { RuntimeTypeInference } from '../types/runtime-inference.js'
import { OpenAPIParser } from '../discovery/openapi-parser.js'
import { TypedAPIProxy } from '../discovery/typed-api-proxy.js'
import { CircuitBreaker } from './circuit-breaker.js'
import { InterceptorChain } from './interceptors.js'
import { RequestMetrics } from './metrics.js'
import { OfflineHandler } from './offline-handler.js'
import { createHttpError, enhanceError, type ErrorContext } from './errors.js'
import type { TypeRegistry, TypedError } from '../types/index.js'
import type { TypedFetchConfig } from '../types/config.js'
import { DEFAULT_CONFIG, mergeConfig } from '../types/config.js'

// Re-export configuration types for convenience
export type { TypedFetchConfig } from '../types/config.js'
export { DEFAULT_CONFIG, mergeConfig } from '../types/config.js'

export class RevolutionaryTypedFetch {
  private config: Required<TypedFetchConfig>
  private cache: WTinyLFUCache
  private deduplicator = new RequestDeduplicator()
  private typeRegistry: TypeRegistry = {}
  private typeInference = new RuntimeTypeInference()
  private openApiParser = new OpenAPIParser()
  private circuitBreaker: CircuitBreaker
  private interceptors = new InterceptorChain()
  private metrics = new RequestMetrics()
  private offlineHandler = new OfflineHandler()
  private baseURL = ''
  
  constructor(config: TypedFetchConfig = {}) {
    this.config = mergeConfig(DEFAULT_CONFIG, config)
    this.cache = new WTinyLFUCache(this.config.cache.maxSize)
    this.circuitBreaker = new CircuitBreaker(
      this.config.circuit.threshold,
      this.config.circuit.timeout
    )
    this.baseURL = this.config.request.baseURL || ''
  }
  
  /**
   * Update configuration dynamically
   */
  configure(config: TypedFetchConfig): void {
    this.config = mergeConfig(this.config, config)
    
    // Reinitialize components that depend on config
    if (config.cache) {
      this.cache = new WTinyLFUCache(this.config.cache.maxSize)
    }
    
    if (config.circuit) {
      this.circuitBreaker = new CircuitBreaker(
        this.config.circuit.threshold,
        this.config.circuit.timeout
      )
    }
    
    // Always update baseURL from config
    this.baseURL = this.config.request.baseURL || ''
  }
  
  /**
   * Create a new instance with custom configuration
   */
  create(config: TypedFetchConfig): RevolutionaryTypedFetch {
    const mergedConfig = mergeConfig(this.config, config)
    return new RevolutionaryTypedFetch(mergedConfig)
  }
  
  // REAL runtime type tracking
  private recordResponse(endpoint: string, method: string, data: any): void {
    const key = `${method.toUpperCase()} ${endpoint}`
    this.typeInference.addSample(key, data)
    
    // Update registry with inferred type
    this.typeRegistry[key] = {
      request: this.typeRegistry[key]?.request,
      response: this.typeInference.inferType(key),
      method: method.toUpperCase(),
      lastSeen: Date.now(),
      samples: [data]
    }
  }
  
  // REAL auto-discovery implementation
  async discover(baseURL?: string): Promise<TypedAPIProxy> {
    // Use provided baseURL or fall back to config
    const discoveryBaseURL = baseURL || this.baseURL || this.config.request.baseURL
    if (!discoveryBaseURL) {
      throw new Error('No baseURL provided for discovery')
    }
    this.baseURL = discoveryBaseURL
    
    try {
      // Try to fetch OpenAPI schema
      const schemaUrls = [
        '/openapi.json',
        '/swagger.json', 
        '/docs/openapi.json',
        '/api/openapi.json',
        '/.well-known/openapi'
      ]
      
      for (const url of schemaUrls) {
        try {
          const response = await fetch(new URL(url, discoveryBaseURL).toString())
          if (response.ok) {
            const schema = await response.json()
            const types = this.openApiParser.parse(schema)
            
            // Merge with existing registry
            Object.assign(this.typeRegistry, types)
            if (this.config.debug.verbose) {
              console.log(`🔍 Discovered ${Object.keys(types).length} endpoints from ${url}`)
            }
            break
          }
        } catch {
          // Continue to next URL
        }
      }
    } catch (error) {
      if (this.config.debug.verbose) {
        console.warn('Schema discovery failed, will use runtime inference')
      }
    }
    
    return new TypedAPIProxy(this, discoveryBaseURL)
  }
  
  // REAL HTTP methods with full type safety
  async get<T = unknown>(url: string, options: RequestInit = {}): Promise<{ data: T; response: Response }> {
    return this.request<T>('GET', url, options)
  }
  
  async post<T = unknown>(url: string, body?: any, options: RequestInit = {}): Promise<{ data: T; response: Response }> {
    return this.request<T>('POST', url, { ...options, body: JSON.stringify(body) })
  }
  
  async put<T = unknown>(url: string, body?: any, options: RequestInit = {}): Promise<{ data: T; response: Response }> {
    return this.request<T>('PUT', url, { ...options, body: JSON.stringify(body) })
  }
  
  async delete<T = unknown>(url: string, options: RequestInit = {}): Promise<{ data: T; response: Response }> {
    return this.request<T>('DELETE', url, options)
  }
  
  private resolveUrl(url: string): string {
    // Use baseURL from config or instance
    const baseURL = this.config.request.baseURL || this.baseURL
    
    // Construct full URL
    if (url.startsWith('http')) {
      return url
    } else if (baseURL) {
      return new URL(url, baseURL).toString()
    } else {
      throw new Error(`Relative URL "${url}" requires a baseURL to be set`)
    }
  }

  private async request<T>(method: string, url: string, options: RequestInit = {}): Promise<{ data: T; response: Response }> {
    const fullUrl = this.resolveUrl(url)
    const cacheKey = `${method}:${fullUrl}`
    const startTime = performance.now()
    let cached = false
    let error: any = null
    
    try {
      // Check cache for GET requests
      if (method === 'GET' && this.config.cache.enabled) {
        const cachedData = this.cache.get(cacheKey)
        if (cachedData) {
          cached = true
          const duration = performance.now() - startTime
          if (this.config.metrics.enabled) {
            this.metrics.recordRequest(fullUrl, duration, cached)
          }
          return { data: cachedData as T, response: new Response('cached') }
        }
      }
      
      // Build request options
      const requestOptions: RequestInit = {
        method,
        ...options,
        headers: {
          ...this.config.request.headers,
          ...(options.headers || {})
        }
      }
      
      // Only set Content-Type for JSON bodies
      if (options.body && typeof options.body === 'string') {
        (requestOptions.headers as any)['Content-Type'] = 'application/json'
      }
      
      // Add timeout if configured
      if (this.config.request.timeout && !requestOptions.signal) {
        const controller = new AbortController()
        setTimeout(() => controller.abort(), this.config.request.timeout)
        requestOptions.signal = controller.signal
      }
      
      // Process through interceptors
      const processedOptions = await this.interceptors.processRequest(requestOptions)
      
      // Handle offline requests
      const result = await this.offlineHandler.handleRequest(fullUrl, processedOptions, async () => {
        // Deduplicate identical requests
        return this.deduplicator.dedupe(cacheKey, async () => {
          // Execute with circuit breaker and retry logic
          return this.executeWithRetry<T>(fullUrl, processedOptions, url, method)
        })
      })
      
      const duration = performance.now() - startTime
      if (this.config.metrics.enabled) {
        this.metrics.recordRequest(fullUrl, duration, cached, error)
      }
      
      // Log successful requests if configured
      if (this.config.debug.logSuccess) {
        console.log(`✅ Request successful: ${method} ${fullUrl} (${duration.toFixed(0)}ms${cached ? ', cached' : ''})`)
      }
      
      return result
      
    } catch (err) {
      error = err
      const duration = performance.now() - startTime
      if (this.config.metrics.enabled) {
        this.metrics.recordRequest(fullUrl, duration, cached, error)
      }
      
      // Log errors if configured
      if (this.config.debug.logErrors) {
        console.error(`❌ Request failed: ${method} ${fullUrl}`, err)
      }
      
      // Create error context
      const errorContext: ErrorContext = {
        method,
        duration
      }
      
      throw enhanceError(err, fullUrl, errorContext)
    }
  }
  
  private async executeWithRetry<T>(fullUrl: string, options: any, originalUrl: string, method: string): Promise<{ data: T; response: Response }> {
    let lastError: any
    const maxAttempts = method === 'GET' ? (this.config.retry.maxAttempts || 1) : 1
    const startTime = performance.now()
    
    for (let attempt = 0; attempt < maxAttempts; attempt++) {
      try {
        const attemptStartTime = performance.now()
        
        // Use circuit breaker if enabled
        const executeRequest = async () => {
          const response = await fetch(fullUrl, options)
          
          if (!response.ok) {
            const errorContext: ErrorContext = {
              method,
              attempt: attempt + 1,
              duration: performance.now() - attemptStartTime
            }
            throw createHttpError(response, fullUrl, errorContext)
          }
          
          const data = await response.json()
          
          // Record response for type inference
          this.recordResponse(originalUrl, method, data)
          
          // Cache successful GET requests
          if (method === 'GET' && this.config.cache.enabled) {
            this.cache.set(`${method}:${fullUrl}`, data, this.config.cache.ttl)
          }
          
          // Process through response interceptors
          const processedResponse = await this.interceptors.processResponse({ data, response })
          
          return processedResponse
        }
        
        // Execute with or without circuit breaker
        if (this.config.circuit.enabled) {
          return await this.circuitBreaker.execute(executeRequest, fullUrl)
        } else {
          return await executeRequest()
        }
      } catch (err) {
        lastError = err
        
        // Check if error is retryable based on config
        const error = err as any
        const isRetryableStatus = error.status && (this.config.retry.retryableStatuses?.includes(error.status) || false)
        const isNetworkError = !error.status && error.type === 'network'
        
        if (!isRetryableStatus && !isNetworkError) {
          // Add error context for non-retryable errors
          if (!error.attempt) {
            const errorContext: ErrorContext = {
              method,
              attempt: attempt + 1,
              duration: performance.now() - startTime
            }
            throw enhanceError(error, fullUrl, errorContext)
          }
          throw error
        }
        
        // Wait before retry (except on last attempt)
        if (attempt < maxAttempts - 1) {
          const delays = this.config.retry.delays || []
          const delay = delays[attempt] || delays[delays.length - 1] || 1000
          
          // Respect Retry-After header if present
          if (error.retryAfter) {
            await this.delay(error.retryAfter)
          } else {
            await this.delay(delay)
          }
        }
      }
    }
    
    // Add final error context
    if (lastError && !lastError.attempt) {
      const errorContext: ErrorContext = {
        method,
        duration: performance.now() - startTime
      }
      if (maxAttempts !== undefined) {
        errorContext.attempt = maxAttempts
      }
      throw enhanceError(lastError, fullUrl, errorContext)
    }
    
    throw lastError || new Error('Request failed after retries')
  }
  
  private async delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms))
  }
  
  // REAL type registry access
  getTypeInfo(endpoint: string): any {
    return this.typeRegistry[endpoint]
  }
  
  getAllTypes(): TypeRegistry {
    return { ...this.typeRegistry }
  }
  
  getInferenceConfidence(endpoint: string): number {
    return this.typeInference.getConfidence(endpoint)
  }
  
  // Advanced features
  addRequestInterceptor(fn: (config: any) => any): void {
    this.interceptors.addRequestInterceptor(fn)
  }
  
  addResponseInterceptor(fn: (response: any) => any): void {
    this.interceptors.addResponseInterceptor(fn)
  }
  
  getMetrics() {
    return this.metrics.getStats()
  }
  
  // Streaming support
  async stream(url: string): Promise<ReadableStream> {
    const fullUrl = this.resolveUrl(url)
    const response = await fetch(fullUrl)
    if (!response.body) throw new Error('No response body')
    return response.body
  }
  
  async *streamJSON(url: string): AsyncGenerator<any> {
    const stream = await this.stream(url)
    const reader = stream.getReader()
    const decoder = new TextDecoder()
    
    let buffer = ''
    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      
      buffer += decoder.decode(value, { stream: true })
      const lines = buffer.split('\n')
      buffer = lines.pop() || ''
      
      for (const line of lines) {
        if (line.trim()) {
          try {
            yield JSON.parse(line)
          } catch {
            // Skip invalid JSON lines
          }
        }
      }
    }
  }
  
  // File upload support
  async upload(url: string, file: File | Blob, options: RequestInit = {}): Promise<{ data: any; response: Response }> {
    const formData = new FormData()
    formData.append('file', file)
    
    return this.request('POST', url, {
      ...options,
      body: formData,
      headers: {
        // Don't set Content-Type for FormData - browser will set it with boundary
        ...options.headers
      }
    })
  }
  
  // GraphQL support
  async graphql(url: string, query: string, variables?: any): Promise<{ data: any; response: Response }> {
    return this.post(url, {
      query,
      variables
    })
  }
  
  // Circuit breaker control
  resetCircuitBreaker(): void {
    this.circuitBreaker.reset()
  }
}