## nimpak/remote/client.nim
## Advanced HTTP client for NimPak remote operations
##
## This module provides an enhanced HTTP client with:
## - Intelligent retry logic with exponential backoff
## - Bandwidth management and throttling
## - Connection pooling and keep-alive
## - Progress reporting for large downloads
## - Compression support (zstd, gzip)

import std/[os, times, httpclient, asyncdispatch, strutils, strformat, json, math, options, random, net, streams]
import ../cli/core, ../security/event_logger

type
  RetryPolicy* = object
    maxRetries*: int
    baseDelay*: int               # Base delay in milliseconds
    maxDelay*: int               # Maximum delay in milliseconds
    backoffMultiplier*: float    # Exponential backoff multiplier
    jitterRange*: float          # Random jitter range (0.0-1.0)

  BandwidthManager* = object
    maxBandwidth*: Option[int]   # Maximum bandwidth in bytes/sec
    currentUsage*: int           # Current bandwidth usage
    activeTransfers*: int        # Number of active transfers
    lastUpdate*: times.DateTime  # Last bandwidth calculation update

  ProgressCallback* = proc(downloaded: int64, total: int64, speed: float)

  DownloadProgress* = object
    downloaded*: int64
    total*: int64
    startTime*: times.DateTime
    lastUpdate*: times.DateTime
    speed*: float                # Bytes per second

  EnhancedHttpClient* = ref object
    client*: HttpClient
    retryPolicy*: RetryPolicy
    bandwidthManager*: BandwidthManager
    userAgent*: string
    timeout*: int
    enableCompression*: bool
    connectionPool*: bool

  DownloadResult*[T] = object
    case success*: bool
    of true:
      data*: T
      progress*: DownloadProgress
    of false:
      error*: string
      errorCode*: int
      retryCount*: int

# =============================================================================
# Client Configuration
# =============================================================================

proc getDefaultRetryPolicy*(): RetryPolicy =
  ## Get default retry policy with exponential backoff
  RetryPolicy(
    maxRetries: 3,
    baseDelay: 1000,             # 1 second
    maxDelay: 30000,             # 30 seconds
    backoffMultiplier: 2.0,
    jitterRange: 0.1             # 10% jitter
  )

proc newBandwidthManager*(maxBandwidth: Option[int] = none(int)): BandwidthManager =
  ## Create a new bandwidth manager
  BandwidthManager(
    maxBandwidth: maxBandwidth,
    currentUsage: 0,
    activeTransfers: 0,
    lastUpdate: now()
  )

proc newEnhancedHttpClient*(userAgent: string = "nimpak/1.0.0",
                           timeout: int = 30000,
                           enableCompression: bool = true,
                           connectionPool: bool = true,
                           retryPolicy: RetryPolicy = getDefaultRetryPolicy(),
                           maxBandwidth: Option[int] = none(int)): EnhancedHttpClient =
  ## Create a new enhanced HTTP client
  var client = newHttpClient(
    timeout = timeout,
    userAgent = userAgent
  )

  # Enable compression
  if enableCompression:
    client.headers["Accept-Encoding"] = "zstd, gzip, deflate"

  # Enable keep-alive for connection pooling
  if connectionPool:
    client.headers["Connection"] = "keep-alive"

  EnhancedHttpClient(
    client: client,
    retryPolicy: retryPolicy,
    bandwidthManager: newBandwidthManager(maxBandwidth),
    userAgent: userAgent,
    timeout: timeout,
    enableCompression: enableCompression,
    connectionPool: connectionPool
  )

# =============================================================================
# Retry Logic with Exponential Backoff
# =============================================================================

proc calculateRetryDelay*(policy: RetryPolicy, attempt: int): int =
  ## Calculate retry delay with exponential backoff and jitter
  let baseDelay = policy.baseDelay.float
  let multiplier = policy.backoffMultiplier
  let exponentialDelay = baseDelay * pow(multiplier, attempt.float)

  # Apply maximum delay limit
  let cappedDelay = min(exponentialDelay, policy.maxDelay.float)

  # Add random jitter to avoid thundering herd
  let jitter = cappedDelay * policy.jitterRange * (rand(1.0) - 0.5)
  let finalDelay = cappedDelay + jitter

  return max(0, finalDelay.int)

proc shouldRetry*(error: ref Exception, attempt: int, policy: RetryPolicy): bool =
  ## Determine if a request should be retried
  if attempt >= policy.maxRetries:
    return false

  # Retry on network errors, timeouts, and 5xx server errors
  if error of HttpRequestError:
    return true
  elif error of TimeoutError:
    return true
  elif error of OSError:
    return true
  else:
    return false

proc executeWithRetry*[T](client: EnhancedHttpClient, operation: proc(): T,
                         operationName: string): DownloadResult[T] =
  ## Execute an operation with retry logic
  var attempt = 0
  var lastError: ref Exception = nil

  while attempt <= client.retryPolicy.maxRetries:
    try:
      let startTime = now()
      let result = operation()
      let duration = (now() - startTime).inMilliseconds.float / 1000.0

      # Log successful operation
      if attempt > 0:
        logGlobalSecurityEvent(EventSystemHealthCheck, SeverityInfo, "http-client",
          fmt"Operation succeeded after {attempt} retries: {operationName}")

      return DownloadResult[T](
        success: true,
        data: result,
        progress: DownloadProgress(
          downloaded: 0,  # Will be set by specific operations
          total: 0,
          startTime: startTime,
          lastUpdate: now(),
          speed: 0.0
        )
      )

    except Exception as e:
      lastError = e

      if not shouldRetry(e, attempt, client.retryPolicy):
        break

      # Calculate retry delay
      let delay = calculateRetryDelay(client.retryPolicy, attempt)

      # Log retry attempt
      logGlobalSecurityEvent(EventSystemHealthCheck, SeverityWarning, "http-client",
        fmt"Operation failed, retrying in {delay}ms: {operationName} - {e.msg}")

      # Wait before retry
      sleep(delay)
      attempt += 1

  # All retries exhausted
  return DownloadResult[T](
    success: false,
    error: if lastError != nil: lastError.msg else: "Unknown error",
    errorCode: 500,
    retryCount: attempt
  )

# =============================================================================
# Bandwidth Management
# =============================================================================

proc updateBandwidthUsage*(manager: var BandwidthManager, bytesTransferred: int) =
  ## Update bandwidth usage statistics
  let now = times.now()
  let timeDiff = (now - manager.lastUpdate).inMilliseconds.float / 1000.0

  if timeDiff > 0:
    # Calculate current speed
    let speed = bytesTransferred.float / timeDiff

    # Update usage (exponential moving average)
    manager.currentUsage = (manager.currentUsage.float * 0.8 + speed * 0.2).int
    manager.lastUpdate = now

proc shouldThrottleTransfer*(manager: BandwidthManager): bool =
  ## Check if transfer should be throttled
  if manager.maxBandwidth.isNone():
    return false

  return manager.currentUsage >= manager.maxBandwidth.get()

proc calculateThrottleDelay*(manager: BandwidthManager, bytesToTransfer: int): int =
  ## Calculate delay needed to stay within bandwidth limits
  if manager.maxBandwidth.isNone():
    return 0

  let maxBandwidth = manager.maxBandwidth.get()
  if manager.currentUsage < maxBandwidth:
    return 0

  # Calculate delay to reduce bandwidth usage
  let excessBandwidth = manager.currentUsage - maxBandwidth
  let delaySeconds = (bytesToTransfer.float / excessBandwidth.float) * 1000.0

  return min(5000, max(0, delaySeconds.int))  # Cap at 5 seconds

# =============================================================================
# Progress Reporting
# =============================================================================

proc newDownloadProgress*(total: int64): DownloadProgress =
  ## Create a new download progress tracker
  DownloadProgress(
    downloaded: 0,
    total: total,
    startTime: now(),
    lastUpdate: now(),
    speed: 0.0
  )

proc updateProgress*(progress: var DownloadProgress, downloaded: int64) =
  ## Update download progress
  let now = times.now()
  let timeDiff = (now - progress.lastUpdate).inMilliseconds.float / 1000.0

  if timeDiff > 0:
    let bytesDiff = downloaded - progress.downloaded
    progress.speed = bytesDiff.float / timeDiff
    progress.downloaded = downloaded
    progress.lastUpdate = now

proc formatProgress*(progress: DownloadProgress): string =
  ## Format progress for display
  let percentage = if progress.total > 0:
    (progress.downloaded.float / progress.total.float * 100.0)
  else:
    0.0

  let speedStr = formatFileSize(progress.speed.int64) & "/s"
  let downloadedStr = formatFileSize(progress.downloaded)
  let totalStr = formatFileSize(progress.total)

  return fmt"{percentage:.1f}% ({downloadedStr}/{totalStr}) at {speedStr}"

# =============================================================================
# Enhanced Download Operations
# =============================================================================

proc downloadWithProgress*(client: EnhancedHttpClient, url: string,
                          progressCallback: ProgressCallback = nil): DownloadResult[seq[byte]] =
  ## Download data with progress reporting and bandwidth management
  let operation = proc(): seq[byte] =
    # Get content length first
    var headClient = newHttpClient(timeout = client.timeout)
    let headResponse = headClient.request(url, httpMethod = HttpHead)
    
    var contentLengthStr = "0"
    if headResponse.headers.hasKey("content-length"):
        contentLengthStr = headResponse.headers["content-length"]
            
    let contentLength = contentLengthStr.parseInt()
    headClient.close()

    # Initialize progress
    var progress = newDownloadProgress(contentLength.int64)
    var data: seq[byte] = @[]

    # Download in chunks for progress reporting
    let chunkSize = 64 * 1024  # 64KB chunks
    var downloaded: int64 = 0

    # Create streaming client
    var streamClient = newHttpClient(timeout = client.timeout)
    let response = streamClient.request(url)

    try:
      while true:
        # Check bandwidth throttling
        if client.bandwidthManager.shouldThrottleTransfer():
          let delay = client.bandwidthManager.calculateThrottleDelay(chunkSize)
          if delay > 0:
            sleep(delay)

        # Read chunk
        let chunk = response.bodyStream.readStr(chunkSize)
        if chunk.len == 0:
          break

        # Add to data
        for b in chunk:
          data.add(b.byte)

        # Update progress
        downloaded += chunk.len
        progress.updateProgress(downloaded)

        # Update bandwidth usage
        client.bandwidthManager.updateBandwidthUsage(chunk.len)

        # Call progress callback
        if progressCallback != nil:
          progressCallback(downloaded, contentLength.int64, progress.speed)

    finally:
      streamClient.close()

    return data

  return client.executeWithRetry(operation, fmt"download {url}")

proc downloadToFile*(client: EnhancedHttpClient, url: string, filePath: string,
                    progressCallback: ProgressCallback = nil): DownloadResult[bool] =
  ## Download data directly to file with progress reporting
  let operation = proc(): bool =
    let downloadResult = client.downloadWithProgress(url, progressCallback)
    if not downloadResult.success:
      raise newException(HttpRequestError, downloadResult.error)

    writeFile(filePath, downloadResult.data)
    return true

  return client.executeWithRetry(operation, fmt"download {url} to {filePath}")

proc uploadWithProgress*(client: EnhancedHttpClient, url: string, data: seq[byte],
                        progressCallback: ProgressCallback = nil): DownloadResult[string] =
  ## Upload data with progress reporting
  let operation = proc(): string =
    # TODO: Implement chunked upload with progress reporting
    # For now, use simple upload
    var body = newString(data.len)
    if data.len > 0:
      copyMem(addr body[0], unsafeAddr data[0], data.len)
    let response = client.client.postContent(url, body)
    return response

  return client.executeWithRetry(operation, fmt"upload to {url}")

# =============================================================================
# Compression Support
# =============================================================================

proc decompressResponse*(data: seq[byte], encoding: string): seq[byte] =
  ## Decompress response data based on encoding
  case encoding.toLower():
  of "gzip":
    # TODO: Implement gzip decompression
    return data
  of "deflate":
    # TODO: Implement deflate decompression
    return data
  of "zstd":
    # TODO: Implement zstd decompression
    return data
  else:
    return data

proc compressData*(data: seq[byte], encoding: string = "gzip"): seq[byte] =
  ## Compress data for upload
  case encoding.toLower():
  of "gzip":
    # TODO: Implement gzip compression
    return data
  of "zstd":
    # TODO: Implement zstd compression
    return data
  else:
    return data

# =============================================================================
# Connection Management
# =============================================================================

proc closeClient*(client: var EnhancedHttpClient) =
  ## Close the HTTP client and clean up resources
  if client.client != nil:
    client.client.close()

proc resetClient*(client: var EnhancedHttpClient) =
  ## Reset the client for reuse
  client.closeClient()
  client.client = newHttpClient(
    timeout = client.timeout,
    userAgent = client.userAgent
  )

  if client.enableCompression:
    client.client.headers["Accept-Encoding"] = "zstd, gzip, deflate"

  if client.connectionPool:
    client.client.headers["Connection"] = "keep-alive"

# =============================================================================
# Statistics and Monitoring
# =============================================================================

type
  ClientStatistics* = object
    requestCount*: int64
    successCount*: int64
    errorCount*: int64
    retryCount*: int64
    bytesDownloaded*: int64
    bytesUploaded*: int64
    averageLatency*: float
    averageSpeed*: float

proc getClientStatistics*(client: EnhancedHttpClient): ClientStatistics =
  ## Get client usage statistics
  # TODO: Implement statistics tracking
  ClientStatistics()

# =============================================================================
# Export main functions
# =============================================================================

export RetryPolicy, BandwidthManager, ProgressCallback, DownloadProgress
export EnhancedHttpClient, DownloadResult, ClientStatistics
export getDefaultRetryPolicy, newBandwidthManager, newEnhancedHttpClient
export calculateRetryDelay, shouldRetry, executeWithRetry
export updateBandwidthUsage, shouldThrottleTransfer, calculateThrottleDelay
export newDownloadProgress, updateProgress, formatProgress
export downloadWithProgress, downloadToFile, uploadWithProgress
export decompressResponse, compressData
export closeClient, resetClient, getClientStatistics