## nimpak/cache/manager.nim
## Binary cache management for NimPak
##
## This module implements intelligent binary caching with:
## - Compatibility detection (CPU flags, libc, allocator, architecture)
## - Automatic binary selection with fallback to source builds
## - Cache eviction policies and performance statistics
## - Integration with verification system for cache integrity

import std/[os, times, json, tables, sequtils, strutils, strformat, algorithm, hashes]
import ../security/[hash_verifier, signature_verifier_working, event_logger]
import ../cli/core
import compatibility, selection

type
  CacheEntryStatus* = enum
    StatusValid = "valid"
    StatusExpired = "expired"
    StatusCorrupted = "corrupted"
    StatusPending = "pending"

  BinaryCacheEntry* = object
    cacheKey*: string              # Unique cache key
    packageName*: string
    version*: string
    platform*: string             # Target platform identifier
    hash*: string                  # Binary hash for verification
    size*: int64                   # Binary size in bytes
    filePath*: string              # Path to cached binary
    compatibility*: CompatibilityInfo
    metadata*: BinaryMetadata
    status*: CacheEntryStatus
    createdAt*: times.DateTime
    lastAccessed*: times.DateTime
    accessCount*: int64
    verified*: bool                # Whether binary has been verified

  CacheIndex* = object
    entries*: Table[string, BinaryCacheEntry]  # cacheKey -> entry
    packageIndex*: Table[string, seq[string]]  # packageName -> cacheKeys
    platformIndex*: Table[string, seq[string]] # platform -> cacheKeys
    sizeIndex*: seq[tuple[size: int64, key: string]]  # Sorted by size for eviction

  EvictionPolicy* = enum
    EvictLRU = "lru"               # Least Recently Used
    EvictLFU = "lfu"               # Least Frequently Used
    EvictSize = "size"             # Largest files first
    EvictAge = "age"               # Oldest files first

  CacheConfig* = object
    cacheDir*: string              # Cache directory path
    maxSize*: int64                # Maximum cache size in bytes
    maxEntries*: int               # Maximum number of entries
    defaultTTL*: int               # Default TTL in seconds
    evictionPolicy*: EvictionPolicy
    verifyOnAccess*: bool          # Verify binaries on access
    enableCompression*: bool       # Enable binary compression
    compressionLevel*: int         # Compression level (1-9)
    cleanupInterval*: int          # Cleanup interval in seconds

  CacheStatistics* = object
    totalEntries*: int64
    totalSize*: int64
    hitCount*: int64
    missCount*: int64
    evictionCount*: int64
    verificationCount*: int64
    corruptionCount*: int64
    hitRate*: float
    averageAccessTime*: float
    lastCleanup*: times.DateTime

  BinaryCacheManager* = object
    config*: CacheConfig
    index*: CacheIndex
    statistics*: CacheStatistics
    compatibilityDetector*: CompatibilityDetector
    binarySelector*: BinarySelector
    lastCleanup*: times.DateTime

  CacheResult*[T] = object
    case success*: bool
    of true:
      value*: T
    of false:
      error*: string
      errorCode*: int

# =============================================================================
# Cache Manager Initialization
# =============================================================================

proc newBinaryCacheManager*(config: CacheConfig): BinaryCacheManager =
  ## Create a new binary cache manager
  # Ensure cache directory exists
  if not dirExists(config.cacheDir):
    createDir(config.cacheDir)

  BinaryCacheManager(
    config: config,
    index: CacheIndex(
      entries: initTable[string, BinaryCacheEntry](),
      packageIndex: initTable[string, seq[string]](),
      platformIndex: initTable[string, seq[string]](),
      sizeIndex: @[]
    ),
    statistics: CacheStatistics(),
    compatibilityDetector: newCompatibilityDetector(),
    binarySelector: newBinarySelector(),
    lastCleanup: now()
  )

proc getDefaultCacheConfig*(): CacheConfig =
  ## Get default cache configuration
  CacheConfig(
    cacheDir: "/var/cache/nimpak/binaries",
    maxSize: 50 * 1024 * 1024 * 1024,  # 50GB
    maxEntries: 10000,
    defaultTTL: 7 * 24 * 3600,         # 7 days
    evictionPolicy: EvictLRU,
    verifyOnAccess: true,
    enableCompression: true,
    compressionLevel: 6,
    cleanupInterval: 3600              # 1 hour
  )

# =============================================================================
# Cache Key Generation
# =============================================================================

proc generateCacheKey*(packageName: string, version: string,
                      compatibility: CompatibilityInfo): string =
  ## Generate unique cache key for binary
  let components = [
    packageName,
    version,
    compatibility.architecture,
    compatibility.libc,
    compatibility.libcVersion,
    compatibility.allocator,
    compatibility.cpuFeatures.sorted().join(","),
    compatibility.abiVersion
  ]

  # Create deterministic hash of components
  let combined = components.join("|")
  let hashResult = computeStringHash(combined, HashBlake2b)

  return fmt"{packageName}-{version}-{hashResult.digest[0..15]}"

proc parseCacheKey*(cacheKey: string): tuple[packageName: string, version: string, hash: string] =
  ## Parse cache key into components
  let parts = cacheKey.split("-")
  if parts.len >= 3:
    let packageName = parts[0]
    let version = parts[1]
    let hash = parts[2..^1].join("-")
    return (packageName, version, hash)
  else:
    return ("", "", "")

# =============================================================================
# Binary Storage and Retrieval
# =============================================================================

proc storeBinary*(manager: var BinaryCacheManager, packageName: string, version: string,
                 binaryData: seq[byte], compatibility: CompatibilityInfo,
                 metadata: BinaryMetadata): CacheResult[BinaryCacheEntry] =
  ## Store binary in cache
  try:
    let cacheKey = generateCacheKey(packageName, version, compatibility)
    let filePath = manager.config.cacheDir / (cacheKey & ".bin")

    # Check if we have space
    let binarySize = binaryData.len.int64
    if manager.statistics.totalSize + binarySize > manager.config.maxSize:
      # Trigger eviction
      let evictResult = manager.evictEntries(binarySize)
      if not evictResult.success:
        return CacheResult[BinaryCacheEntry](success: false, error: evictResult.error, errorCode: evictResult.errorCode)

    # Compress binary if enabled
    let finalData = if manager.config.enableCompression:
      compressBinary(binaryData, manager.config.compressionLevel)
    else:
      binaryData

    # Calculate hash
    let hashResult = computeStringHash(finalData, HashBlake2b)
    let binaryHash = formatHashString(hashResult.algorithm, hashResult.digest)

    # Write binary to disk
    writeFile(filePath, finalData)

    # Create cache entry
    let entry = BinaryCacheEntry(
      cacheKey: cacheKey,
      packageName: packageName,
      version: version,
      platform: formatPlatformString(compatibility),
      hash: binaryHash,
      size: binarySize,
      filePath: filePath,
      compatibility: compatibility,
      metadata: metadata,
      status: StatusValid,
      createdAt: now(),
      lastAccessed: now(),
      accessCount: 0,
      verified: true
    )

    # Add to index
    manager.index.entries[cacheKey] = entry

    # Update package index
    if packageName notin manager.index.packageIndex:
      manager.index.packageIndex[packageName] = @[]
    manager.index.packageIndex[packageName].add(cacheKey)

    # Update platform index
    let platformKey = formatPlatformString(compatibility)
    if platformKey notin manager.index.platformIndex:
      manager.index.platformIndex[platformKey] = @[]
    manager.index.platformIndex[platformKey].add(cacheKey)

    # Update size index
    manager.index.sizeIndex.add((binarySize, cacheKey))
    manager.index.sizeIndex.sort(proc(a, b: tuple[size: int64, key: string]): int = cmp(b.size, a.size))

    # Update statistics
    manager.statistics.totalEntries += 1
    manager.statistics.totalSize += binarySize

    # Log cache storage
    logGlobalSecurityEvent(EventSystemHealthCheck, SeverityInfo, "binary-cache",
      fmt"Binary cached: {packageName} v{version} ({formatFileSize(binarySize)})")

    return CacheResult[BinaryCacheEntry](success: true, value: entry)

  except Exception as e:
    return CacheResult[BinaryCacheEntry](success: false, error: fmt"Failed to store binary: {e.msg}", errorCode: 500)

proc retrieveBinary*(manager: var BinaryCacheManager, cacheKey: string): CacheResult[seq[byte]] =
  ## Retrieve binary from cache
  let startTime = cpuTime()

  try:
    if cacheKey notin manager.index.entries:
      manager.statistics.missCount += 1
      return CacheResult[seq[byte]](success: false, error: "Cache entry not found", errorCode: 404)

    var entry = manager.index.entries[cacheKey]

    # Check if entry is valid
    if entry.status != StatusValid:
      manager.statistics.missCount += 1
      return CacheResult[seq[byte]](success: false, error: fmt"Cache entry invalid: {entry.status}", errorCode: 410)

    # Check if file exists
    if not fileExists(entry.filePath):
      # Mark as corrupted
      entry.status = StatusCorrupted
      manager.index.entries[cacheKey] = entry
      manager.statistics.corruptionCount += 1
      return CacheResult[seq[byte]](success: false, error: "Cache file not found", errorCode: 404)

    # Read binary data
    let fileData = readFile(entry.filePath).toOpenArrayByte(0, -1)
    var binaryData = newSeq[byte](fileData.len)
    for i, b in fileData:
      binaryData[i] = b

    # Verify hash if configured
    if manager.config.verifyOnAccess:
      let hashResult = computeStringHash(binaryData, HashBlake2b)
      let actualHash = formatHashString(hashResult.algorithm, hashResult.digest)

      if actualHash != entry.hash:
        # Mark as corrupted
        entry.status = StatusCorrupted
        manager.index.entries[cacheKey] = entry
        manager.statistics.corruptionCount += 1
        return CacheResult[seq[byte]](success: false, error: "Hash verification failed", errorCode: 409)

      manager.statistics.verificationCount += 1

    # Decompress if needed
    let finalData = if manager.config.enableCompression:
      decompressBinary(binaryData)
    else:
      binaryData

    # Update access statistics
    entry.lastAccessed = now()
    entry.accessCount += 1
    manager.index.entries[cacheKey] = entry

    manager.statistics.hitCount += 1
    manager.statistics.hitRate = manager.statistics.hitCount.float /
                                (manager.statistics.hitCount + manager.statistics.missCount).float

    let accessTime = cpuTime() - startTime
    manager.statistics.averageAccessTime = (manager.statistics.averageAccessTime + accessTime) / 2.0

    return CacheResult[seq[byte]](success: true, value: finalData)

  except Exception as e:
    manager.statistics.missCount += 1
    return CacheResult[seq[byte]](success: false, error: fmt"Failed to retrieve binary: {e.msg}", errorCode: 500)

# =============================================================================
# Binary Search and Selection
# =============================================================================

proc findCompatibleBinaries*(manager: BinaryCacheManager, packageName: string, version: string,
                            targetSystem: SystemInfo): seq[BinaryCacheEntry] =
  ## Find compatible binaries for target system
  var compatibleEntries: seq[BinaryCacheEntry] = @[]

  # Get all entries for this package
  if packageName notin manager.index.packageIndex:
    return compatibleEntries

  for cacheKey in manager.index.packageIndex[packageName]:
    if cacheKey in manager.index.entries:
      let entry = manager.index.entries[cacheKey]

      # Check version match
      if entry.version != version:
        continue

      # Check compatibility
      let compatibility = manager.compatibilityDetector.checkCompatibility(
        entry.compatibility, targetSystem
      )

      if compatibility.compatible:
        compatibleEntries.add(entry)

  # Sort by compatibility score (best match first)
  compatibleEntries.sort(proc(a, b: BinaryCacheEntry): int =
    let scoreA = manager.compatibilityDetector.calculateCompatibilityScore(a.compatibility, targetSystem)
    let scoreB = manager.compatibilityDetector.calculateCompatibilityScore(b.compatibility, targetSystem)
    return cmp(scoreB, scoreA)
  )

  return compatibleEntries

proc selectBestBinary*(manager: BinaryCacheManager, packageName: string, version: string,
                      targetSystem: SystemInfo): CacheResult[BinaryCacheEntry] =
  ## Select the best compatible binary for target system
  try:
    let compatibleBinaries = manager.findCompatibleBinaries(packageName, version, targetSystem)

    if compatibleBinaries.len == 0:
      return CacheResult[BinaryCacheEntry](success: false, error: "No compatible binaries found", errorCode: 404)

    # Use binary selector for intelligent selection
    let selectionResult = manager.binarySelector.selectOptimalBinary(compatibleBinaries, targetSystem)
    if not selectionResult.success:
      return CacheResult[BinaryCacheEntry](success: false, error: selectionResult.error, errorCode: selectionResult.errorCode)

    return CacheResult[BinaryCacheEntry](success: true, value: selectionResult.value)

  except Exception as e:
    return CacheResult[BinaryCacheEntry](success: false, error: fmt"Binary selection failed: {e.msg}", errorCode: 500)

# =============================================================================
# Cache Eviction
# =============================================================================

proc evictEntries*(manager: var BinaryCacheManager, spaceNeeded: int64): CacheResult[int] =
  ## Evict cache entries to free space
  try:
    var freedSpace: int64 = 0
    var evictedCount = 0
    var entriesToEvict: seq[string] = @[]

    case manager.config.evictionPolicy:
    of EvictLRU:
      # Sort by last accessed (oldest first)
      var sortedEntries = manager.index.entries.values.toSeq
      sortedEntries.sort(proc(a, b: BinaryCacheEntry): int = cmp(a.lastAccessed, b.lastAccessed))

      for entry in sortedEntries:
        if freedSpace >= spaceNeeded:
          break
        entriesToEvict.add(entry.cacheKey)
        freedSpace += entry.size

    of EvictLFU:
      # Sort by access count (least used first)
      var sortedEntries = manager.index.entries.values.toSeq
      sortedEntries.sort(proc(a, b: BinaryCacheEntry): int = cmp(a.accessCount, b.accessCount))

      for entry in sortedEntries:
        if freedSpace >= spaceNeeded:
          break
        entriesToEvict.add(entry.cacheKey)
        freedSpace += entry.size

    of EvictSize:
      # Use size index (largest first)
      for (size, key) in manager.index.sizeIndex:
        if freedSpace >= spaceNeeded:
          break
        if key in manager.index.entries:
          entriesToEvict.add(key)
          freedSpace += size

    of EvictAge:
      # Sort by creation time (oldest first)
      var sortedEntries = manager.index.entries.values.toSeq
      sortedEntries.sort(proc(a, b: BinaryCacheEntry): int = cmp(a.createdAt, b.createdAt))

      for entry in sortedEntries:
        if freedSpace >= spaceNeeded:
          break
        entriesToEvict.add(entry.cacheKey)
        freedSpace += entry.size

    # Perform eviction
    for cacheKey in entriesToEvict:
      let removeResult = manager.removeCacheEntry(cacheKey)
      if removeResult.success:
        evictedCount += 1

    # Update statistics
    manager.statistics.evictionCount += evictedCount.int64

    # Log eviction
    logGlobalSecurityEvent(EventSystemHealthCheck, SeverityInfo, "binary-cache",
      fmt"Cache eviction completed: {evictedCount} entries, {formatFileSize(freedSpace)} freed")

    return CacheResult[int](success: true, value: evictedCount)

  except Exception as e:
    return CacheResult[int](success: false, error: fmt"Eviction failed: {e.msg}", errorCode: 500)

proc removeCacheEntry*(manager: var BinaryCacheManager, cacheKey: string): CacheResult[bool] =
  ## Remove a specific cache entry
  try:
    if cacheKey notin manager.index.entries:
      return CacheResult[bool](success: false, error: "Cache entry not found", errorCode: 404)

    let entry = manager.index.entries[cacheKey]

    # Remove file
    if fileExists(entry.filePath):
      removeFile(entry.filePath)

    # Remove from indices
    manager.index.entries.del(cacheKey)

    # Remove from package index
    if entry.packageName in manager.index.packageIndex:
      let packageKeys = manager.index.packageIndex[entry.packageName].filterIt(it != cacheKey)
      if packageKeys.len == 0:
        manager.index.packageIndex.del(entry.packageName)
      else:
        manager.index.packageIndex[entry.packageName] = packageKeys

    # Remove from platform index
    if entry.platform in manager.index.platformIndex:
      let platformKeys = manager.index.platformIndex[entry.platform].filterIt(it != cacheKey)
      if platformKeys.len == 0:
        manager.index.platformIndex.del(entry.platform)
      else:
        manager.index.platformIndex[entry.platform] = platformKeys

    # Remove from size index
    manager.index.sizeIndex = manager.index.sizeIndex.filterIt(it.key != cacheKey)

    # Update statistics
    manager.statistics.totalEntries -= 1
    manager.statistics.totalSize -= entry.size

    return CacheResult[bool](success: true, value: true)

  except Exception as e:
    return CacheResult[bool](success: false, error: fmt"Failed to remove cache entry: {e.msg}", errorCode: 500)

# =============================================================================
# Cache Maintenance
# =============================================================================

proc cleanupExpiredEntries*(manager: var BinaryCacheManager): CacheResult[int] =
  ## Clean up expired cache entries
  try:
    var expiredKeys: seq[string] = @[]
    let now = times.now()

    for cacheKey, entry in manager.index.entries.pairs:
      let age = (now - entry.createdAt).inSeconds
      if age > manager.config.defaultTTL:
        expiredKeys.add(cacheKey)

    # Remove expired entries
    var removedCount = 0
    for cacheKey in expiredKeys:
      let removeResult = manager.removeCacheEntry(cacheKey)
      if removeResult.success:
        removedCount += 1

    manager.statistics.lastCleanup = now

    if removedCount > 0:
      logGlobalSecurityEvent(EventSystemHealthCheck, SeverityInfo, "binary-cache",
        fmt"Cache cleanup completed: {removedCount} expired entries removed")

    return CacheResult[int](success: true, value: removedCount)

  except Exception as e:
    return CacheResult[int](success: false, error: fmt"Cleanup failed: {e.msg}", errorCode: 500)

proc verifyAllEntries*(manager: var BinaryCacheManager): CacheResult[tuple[valid: int, corrupted: int]] =
  ## Verify all cache entries
  try:
    var validCount = 0
    var corruptedCount = 0

    for cacheKey in manager.index.entries.keys.toSeq:
      let retrieveResult = manager.retrieveBinary(cacheKey)
      if retrieveResult.success:
        validCount += 1
      else:
        corruptedCount += 1

    logGlobalSecurityEvent(EventSystemHealthCheck, SeverityInfo, "binary-cache",
      fmt"Cache verification completed: {validCount} valid, {corruptedCount} corrupted")

    return CacheResult[tuple[valid: int, corrupted: int]](success: true, value: (validCount, corruptedCount))

  except Exception as e:
    return CacheResult[tuple[valid: int, corrupted: int]](success: false, error: fmt"Verification failed: {e.msg}", errorCode: 500)

proc performMaintenance*(manager: var BinaryCacheManager): CacheResult[bool] =
  ## Perform comprehensive cache maintenance
  try:
    let now = times.now()
    let timeSinceLastCleanup = (now - manager.lastCleanup).inSeconds

    if timeSinceLastCleanup >= manager.config.cleanupInterval:
      # Clean up expired entries
      discard manager.cleanupExpiredEntries()

      # Check if we need to evict entries due to size limits
      if manager.statistics.totalSize > manager.config.maxSize:
        let spaceToFree = manager.statistics.totalSize - (manager.config.maxSize * 90 div 100)  # Free to 90%
        discard manager.evictEntries(spaceToFree)

      # Check if we need to evict entries due to count limits
      if manager.statistics.totalEntries > manager.config.maxEntries.int64:
        let entriesToRemove = manager.statistics.totalEntries - (manager.config.maxEntries * 90 div 100).int64
        # TODO: Implement count-based eviction
        discard

      manager.lastCleanup = now

    return CacheResult[bool](success: true, value: true)

  except Exception as e:
    return CacheResult[bool](success: false, error: fmt"Maintenance failed: {e.msg}", errorCode: 500)

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

proc compressBinary*(data: seq[byte], level: int): seq[byte] =
  ## Compress binary data
  # TODO: Implement actual compression (zstd, lz4, etc.)
  # For now, return original data
  return data

proc decompressBinary*(data: seq[byte]): seq[byte] =
  ## Decompress binary data
  # TODO: Implement actual decompression
  # For now, return original data
  return data

# =============================================================================
# Cache Statistics and Reporting
# =============================================================================

proc getCacheStatistics*(manager: BinaryCacheManager): CacheStatistics =
  ## Get current cache statistics
  return manager.statistics

proc getCacheReport*(manager: BinaryCacheManager): JsonNode =
  ## Generate comprehensive cache report
  let stats = manager.statistics

  return %*{
    "cache_statistics": {
      "total_entries": stats.totalEntries,
      "total_size": stats.totalSize,
      "total_size_formatted": formatFileSize(stats.totalSize),
      "hit_count": stats.hitCount,
      "miss_count": stats.missCount,
      "hit_rate": stats.hitRate,
      "eviction_count": stats.evictionCount,
      "verification_count": stats.verificationCount,
      "corruption_count": stats.corruptionCount,
      "average_access_time": stats.averageAccessTime,
      "last_cleanup": $stats.lastCleanup
    },
    "cache_configuration": {
      "cache_dir": manager.config.cacheDir,
      "max_size": manager.config.maxSize,
      "max_size_formatted": formatFileSize(manager.config.maxSize),
      "max_entries": manager.config.maxEntries,
      "eviction_policy": $manager.config.evictionPolicy,
      "verify_on_access": manager.config.verifyOnAccess,
      "compression_enabled": manager.config.enableCompression
    },
    "index_statistics": {
      "packages_cached": manager.index.packageIndex.len,
      "platforms_supported": manager.index.platformIndex.len,
      "size_index_entries": manager.index.sizeIndex.len
    }
  }

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

export CacheEntryStatus, BinaryCacheEntry, CacheIndex, EvictionPolicy
export CacheConfig, CacheStatistics, BinaryCacheManager, CacheResult
export newBinaryCacheManager, getDefaultCacheConfig
export generateCacheKey, parseCacheKey
export storeBinary, retrieveBinary
export findCompatibleBinaries, selectBestBinary
export evictEntries, removeCacheEntry
export cleanupExpiredEntries, verifyAllEntries, performMaintenance
export compressBinary, decompressBinary
export getCacheStatistics, getCacheReport