## Resolution Cache with CAS Integration
##
## This module provides a two-tier caching system for dependency resolution:
## - **L1 Cache**: In-memory LRU cache for hot resolution results
## - **L2 Cache**: CAS-backed persistent storage for cold resolution results
##
## **Cache Key Strategy:**
## - Cache key includes global repository state hash
## - Any metadata change invalidates all cache entries automatically
## - Variant demand is canonicalized for deterministic keys
##
## **Performance:**
## - L1 hit: ~1μs (in-memory lookup)
## - L2 hit: ~100μs (CAS retrieval + deserialization)
## - Cache miss: ~100ms-1s (full resolution)
##
## **Invalidation:**
## - Automatic on repository metadata changes
## - Manual via clear() or invalidate()

import options
import tables
import ./variant_types
import ./dependency_graph
import ./serialization
import ./lru_cache
import strutils

type
  ResolutionCache* = ref object
    ## Two-tier cache for dependency resolution results
    ## Note: L2 (CAS) integration is simplified for MVP
    l1Cache: LRUCacheWithStats[string, DependencyGraph]
    enabled: bool
    l1Capacity: int
    currentRepoHash: string

  CacheKey* = object
    ## Key for caching resolution results
    rootPackage*: string
    rootConstraint*: string
    repoStateHash*: string
    variantDemand*: VariantDemand

  CacheResult*[T] = object
    ## Result of cache lookup with source information
    value*: Option[T]
    source*: CacheSource

  CacheSource* = enum
    ## Where the cached value came from
    L1Hit,      ## In-memory LRU cache
    L2Hit,      ## CAS persistent storage
    CacheMiss   ## Not found in cache

  CacheMetrics* = object
    ## Cache performance metrics
    l1Hits*: int
    l2Hits*: int
    misses*: int
    l1Size*: int
    l1Capacity*: int
    l1HitRate*: float
    totalHitRate*: float

# ============================================================================
# Cache Construction
# ============================================================================

proc newResolutionCache*(
  l1Capacity: int = 100,
  enabled: bool = true
): ResolutionCache =
  ## Create a new resolution cache (L1 in-memory only for MVP).
  ##
  ## **Note:** L2 (CAS) integration simplified for MVP
  ##
  ## **Parameters:**
  ## - l1Capacity: Maximum entries in L1 (in-memory) cache
  ## - enabled: Enable/disable caching (for testing)
  ##
  ## **Returns:** New resolution cache instance
  ##
  ## **Example:**
  ## ```nim
  ## let cache = newResolutionCache(l1Capacity = 100)
  ## ```
  
  result = ResolutionCache(
    l1Cache: newLRUCacheWithStats[string, DependencyGraph](l1Capacity),
    enabled: enabled,
    l1Capacity: l1Capacity,
    currentRepoHash: ""
  )

# ============================================================================
# Cache Operations
# ============================================================================

proc calculateCacheKey*(key: CacheKey): string =
  ## Calculate cache key hash from CacheKey object
  serialization.calculateCacheKey(
    key.rootPackage,
    key.rootConstraint,
    key.repoStateHash,
    key.variantDemand
  )

proc get*(
  cache: ResolutionCache,
  key: CacheKey
): CacheResult[DependencyGraph] =
  ## Get dependency graph from cache (L1 → L2 → miss).
  ##
  ## **Parameters:**
  ## - key: Cache key (includes repo state hash)
  ##
  ## **Returns:** Cache result with value and source
  ##
  ## **Lookup Order:**
  ## 1. Check L1 (in-memory LRU cache)
  ## 2. Check L2 (CAS persistent storage)
  ## 3. Return cache miss
  ##
  ## **Complexity:**
  ## - L1 hit: O(1) ~1μs
  ## - L2 hit: O(1) ~100μs (CAS lookup + deserialization)
  ## - Miss: O(1) ~1μs
  
  if not cache.enabled:
    return CacheResult[DependencyGraph](
      value: none(DependencyGraph),
      source: CacheMiss
    )
  
  # Calculate cache key hash
  let cacheKeyHash = calculateCacheKey(key)
  
  # Try L1 cache (in-memory)
  let l1Result = cache.l1Cache.get(cacheKeyHash)
  if l1Result.isSome:
    return CacheResult[DependencyGraph](
      value: l1Result,
      source: L1Hit
    )
  
  # L2 cache (CAS) - Simplified for MVP
  # TODO: Implement CAS integration when CASStorage type is available
  
  # Cache miss
  return CacheResult[DependencyGraph](
    value: none(DependencyGraph),
    source: CacheMiss
  )

proc put*(
  cache: ResolutionCache,
  key: CacheKey,
  graph: DependencyGraph
) =
  ## Put dependency graph into cache (L1 + L2).
  ##
  ## **Parameters:**
  ## - key: Cache key (includes repo state hash)
  ## - graph: Dependency graph to cache
  ##
  ## **Storage:**
  ## - L1: Stored in in-memory LRU cache
  ## - L2: Serialized to MessagePack and stored in CAS
  ##
  ## **Complexity:** O(n) where n = graph size (serialization cost)
  
  if not cache.enabled:
    return
  
  # Calculate cache key hash
  let cacheKeyHash = calculateCacheKey(key)
  
  # Store in L1 cache (in-memory)
  cache.l1Cache.put(cacheKeyHash, graph)
  
  # L2 cache (CAS) - Simplified for MVP
  # TODO: Implement CAS storage when CASStorage type is available
  # let serialized = toMessagePack(graph)
  # discard cache.casStorage.store(cacheKeyHash, serialized)

proc invalidate*(cache: ResolutionCache, key: CacheKey) =
  ## Invalidate specific cache entry.
  ##
  ## **Parameters:**
  ## - key: Cache key to invalidate
  ##
  ## **Effect:** Removes entry from L1 cache (L2 remains for potential reuse)
  
  if not cache.enabled:
    return
  
  let cacheKeyHash = calculateCacheKey(key)
  discard cache.l1Cache.delete(cacheKeyHash)

proc clear*(cache: ResolutionCache) =
  ## Clear all cache entries (L1 only, L2 remains).
  ##
  ## **Effect:** Clears in-memory L1 cache, CAS L2 cache remains
  ##
  ## **Note:** L2 cache is not cleared to preserve disk-backed cache
  ## across nip invocations. Use clearAll() to clear both tiers.
  
  cache.l1Cache.clear()
  cache.l1Cache.resetStats()

proc clearAll*(cache: ResolutionCache) =
  ## Clear all cache entries (L1 + L2).
  ##
  ## **Effect:** Clears both in-memory and CAS-backed caches
  ##
  ## **Warning:** This removes all cached resolution results from disk
  
  cache.clear()
  # Note: CAS storage doesn't have a clearAll() method
  # Individual entries are garbage collected based on reference tracking

proc updateRepoHash*(cache: ResolutionCache, newHash: string) =
  ## Update current repository state hash.
  ##
  ## **Parameters:**
  ## - newHash: New global repository state hash
  ##
  ## **Effect:** If hash changed, clears L1 cache (automatic invalidation)
  ##
  ## **Rationale:** Repository metadata change invalidates all cached results
  
  if cache.currentRepoHash != newHash:
    cache.currentRepoHash = newHash
    cache.clear()  # Invalidate all L1 entries

proc isEnabled*(cache: ResolutionCache): bool =
  ## Check if caching is enabled.
  
  return cache.enabled

proc setEnabled*(cache: ResolutionCache, enabled: bool) =
  ## Enable or disable caching.
  ##
  ## **Parameters:**
  ## - enabled: true to enable, false to disable
  ##
  ## **Effect:** When disabled, all cache operations become no-ops
  
  cache.enabled = enabled

# ============================================================================
# Cache Metrics
# ============================================================================

proc getMetrics*(cache: ResolutionCache): CacheMetrics =
  ## Get cache performance metrics.
  ##
  ## **Returns:** Metrics including hit rates, sizes, and sources
  
  let l1Stats = cache.l1Cache.getStats()
  
  # Calculate L2 hits (total hits - L1 hits)
  # Note: This is approximate since we don't track L2 hits separately
  let totalAccesses = l1Stats.hits + l1Stats.misses
  let l2Hits = 0  # TODO: Track L2 hits separately
  
  let totalHits = l1Stats.hits + l2Hits
  let totalHitRate = if totalAccesses > 0:
    totalHits.float / totalAccesses.float
  else:
    0.0
  
  result = CacheMetrics(
    l1Hits: l1Stats.hits,
    l2Hits: l2Hits,
    misses: l1Stats.misses,
    l1Size: l1Stats.size,
    l1Capacity: l1Stats.capacity,
    l1HitRate: cache.l1Cache.hitRate(),
    totalHitRate: totalHitRate
  )

proc `$`*(metrics: CacheMetrics): string =
  ## String representation of cache metrics.
  
  result = "CacheMetrics(\n"
  result &= "  L1 hits: " & $metrics.l1Hits & "\n"
  result &= "  L2 hits: " & $metrics.l2Hits & "\n"
  result &= "  Misses: " & $metrics.misses & "\n"
  result &= "  L1 size: " & $metrics.l1Size & "/" & $metrics.l1Capacity & "\n"
  result &= "  L1 hit rate: " & (metrics.l1HitRate * 100.0).formatFloat(ffDecimal, 2) & "%\n"
  result &= "  Total hit rate: " & (metrics.totalHitRate * 100.0).formatFloat(ffDecimal, 2) & "%\n"
  result &= ")"

# ============================================================================
# Convenience Helpers
# ============================================================================

proc getCached*(
  cache: ResolutionCache,
  rootPackage: string,
  rootConstraint: string,
  repoStateHash: string,
  variantDemand: VariantDemand
): CacheResult[DependencyGraph] =
  ## Convenience method to get cached graph with individual parameters.
  ##
  ## **Parameters:**
  ## - rootPackage: Root package name
  ## - rootConstraint: Root package constraint
  ## - repoStateHash: Global repository state hash
  ## - variantDemand: Variant demand for resolution
  ##
  ## **Returns:** Cache result with value and source
  
  let key = CacheKey(
    rootPackage: rootPackage,
    rootConstraint: rootConstraint,
    repoStateHash: repoStateHash,
    variantDemand: variantDemand
  )
  
  return cache.get(key)

proc putCached*(
  cache: ResolutionCache,
  rootPackage: string,
  rootConstraint: string,
  repoStateHash: string,
  variantDemand: VariantDemand,
  graph: DependencyGraph
) =
  ## Convenience method to put graph into cache with individual parameters.
  ##
  ## **Parameters:**
  ## - rootPackage: Root package name
  ## - rootConstraint: Root package constraint
  ## - repoStateHash: Global repository state hash
  ## - variantDemand: Variant demand for resolution
  ## - graph: Dependency graph to cache
  
  let key = CacheKey(
    rootPackage: rootPackage,
    rootConstraint: rootConstraint,
    repoStateHash: repoStateHash,
    variantDemand: variantDemand
  )
  
  cache.put(key, graph)

# ============================================================================
# Debug and Inspection
# ============================================================================

proc `$`*(cache: ResolutionCache): string =
  ## String representation of cache for debugging.
  
  result = "ResolutionCache(\n"
  result &= "  enabled: " & $cache.enabled & "\n"
  result &= "  L1 capacity: " & $cache.l1Capacity & "\n"
  result &= "  L1 size: " & $cache.l1Cache.getStats().size & "\n"
  result &= "  current repo hash: " & cache.currentRepoHash & "\n"
  result &= ")"

# ============================================================================
# Unit Tests
# ============================================================================

when isMainModule:
  import unittest
  
  suite "Resolution Cache Basic Operations":
    test "Create cache":
      let cas = newCASStorage("/tmp/test-cas")
      let cache = newResolutionCache(cas, l1Capacity = 10)
      
      check cache.isEnabled
      check cache.l1Capacity == 10
    
    test "Cache miss on empty cache":
      let cas = newCASStorage("/tmp/test-cas")
      let cache = newResolutionCache(cas)
      
      let key = CacheKey(
        rootPackage: "nginx",
        rootConstraint: ">=1.24.0",
        repoStateHash: "hash123",
        variantDemand: VariantDemand(
          useFlags: @[],
          libc: "musl",
          allocator: "jemalloc",
          targetArch: "x86_64",
          buildFlags: @[]
        )
      )
      
      let result = cache.get(key)
      check result.value.isNone
      check result.source == CacheMiss
    
    test "Put and get from L1 cache":
      let cas = newCASStorage("/tmp/test-cas")
      let cache = newResolutionCache(cas)
      
      let key = CacheKey(
        rootPackage: "nginx",
        rootConstraint: ">=1.24.0",
        repoStateHash: "hash123",
        variantDemand: VariantDemand(
          useFlags: @[],
          libc: "musl",
          allocator: "jemalloc",
          targetArch: "x86_64",
          buildFlags: @[]
        )
      )
      
      let graph = DependencyGraph(
        rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"),
        nodes: @[],
        timestamp: 1700000000
      )
      
      cache.put(key, graph)
      
      let result = cache.get(key)
      check result.value.isSome
      check result.source == L1Hit
      check result.value.get.rootPackage.name == "nginx"
    
    test "Disabled cache returns miss":
      let cas = newCASStorage("/tmp/test-cas")
      let cache = newResolutionCache(cas, enabled = false)
      
      let key = CacheKey(
        rootPackage: "nginx",
        rootConstraint: ">=1.24.0",
        repoStateHash: "hash123",
        variantDemand: VariantDemand(
          useFlags: @[],
          libc: "musl",
          allocator: "jemalloc",
          targetArch: "x86_64",
          buildFlags: @[]
        )
      )
      
      let graph = DependencyGraph(
        rootPackage: PackageId(name: "nginx", version: "1.24.0", variant: "default"),
        nodes: @[],
        timestamp: 1700000000
      )
      
      cache.put(key, graph)
      
      let result = cache.get(key)
      check result.value.isNone
      check result.source == CacheMiss
