## Persistent Cache Index with SQLite
##
## This module provides an optional SQLite-backed persistent cache index
## that survives across nip invocations. This enables:
## - Fast cache lookups without CAS scanning
## - Cache statistics persistence
## - Cache metadata storage
## - Cross-session cache reuse
##
## **Architecture:**
## - SQLite database stores cache keys → CAS IDs mapping
## - Actual graph data stored in CAS (content-addressable)
## - Index provides O(1) lookup without CAS scanning
##
## **Use Cases:**
## - Persistent caching across nip invocations
## - Fast cache warmup on startup
## - Cache statistics tracking over time
## - Debugging and cache inspection

import db_sqlite
import options
import times
import ./variant_types
import ./dependency_graph

type
  PersistentCacheIndex* = ref object
    ## SQLite-backed persistent cache index
    db: DbConn
    dbPath: string
    enabled: bool

  CacheEntry* = object
    ## Cache entry metadata
    cacheKey*: string
    casId*: string
    timestamp*: DateTime
    hitCount*: int
    lastAccess*: DateTime

  CacheIndexStats* = object
    ## Persistent cache statistics
    totalEntries*: int
    totalHits*: int
    oldestEntry*: DateTime
    newestEntry*: DateTime
    dbSize*: int64

# ============================================================================
# Database Schema
# ============================================================================

const SCHEMA_VERSION = 1

const CREATE_TABLES = """
CREATE TABLE IF NOT EXISTS cache_entries (
  cache_key TEXT PRIMARY KEY,
  cas_id TEXT NOT NULL,
  timestamp INTEGER NOT NULL,
  hit_count INTEGER DEFAULT 0,
  last_access INTEGER NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_last_access ON cache_entries(last_access);
CREATE INDEX IF NOT EXISTS idx_timestamp ON cache_entries(timestamp);

CREATE TABLE IF NOT EXISTS cache_metadata (
  key TEXT PRIMARY KEY,
  value TEXT NOT NULL
);

INSERT OR IGNORE INTO cache_metadata (key, value) VALUES ('schema_version', '1');
"""

# ============================================================================
# Index Construction
# ============================================================================

proc newPersistentCacheIndex*(
  dbPath: string,
  enabled: bool = true
): PersistentCacheIndex =
  ## Create or open persistent cache index.
  ##
  ## **Parameters:**
  ## - dbPath: Path to SQLite database file
  ## - enabled: Enable/disable persistent caching
  ##
  ## **Returns:** New persistent cache index
  ##
  ## **Example:**
  ## ```nim
  ## let index = newPersistentCacheIndex("/var/lib/nip/cache.db")
  ## ```
  
  result = PersistentCacheIndex(
    dbPath: dbPath,
    enabled: enabled
  )
  
  if enabled:
    # Open or create database
    result.db = open(dbPath, "", "", "")
    
    # Create schema
    result.db.exec(sql(CREATE_TABLES))

proc close*(index: PersistentCacheIndex) =
  ## Close database connection.
  
  if index.enabled and not index.db.isNil:
    index.db.close()

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

proc get*(index: PersistentCacheIndex, cacheKey: string): Option[string] =
  ## Get CAS ID for cache key.
  ##
  ## **Parameters:**
  ## - cacheKey: Cache key to lookup
  ##
  ## **Returns:** Some(casId) if found, None if not found
  ##
  ## **Side Effect:** Updates hit count and last access time
  
  if not index.enabled:
    return none(string)
  
  let row = index.db.getRow(sql"""
    SELECT cas_id FROM cache_entries WHERE cache_key = ?
  """, cacheKey)
  
  if row[0].len > 0:
    # Update hit count and last access
    index.db.exec(sql"""
      UPDATE cache_entries
      SET hit_count = hit_count + 1,
          last_access = ?
      WHERE cache_key = ?
    """, now().toTime().toUnix(), cacheKey)
    
    return some(row[0])
  else:
    return none(string)

proc put*(index: PersistentCacheIndex, cacheKey: string, casId: string) =
  ## Store cache key → CAS ID mapping.
  ##
  ## **Parameters:**
  ## - cacheKey: Cache key
  ## - casId: CAS ID where graph is stored
  ##
  ## **Effect:** Inserts or updates cache entry
  
  if not index.enabled:
    return
  
  let now = now().toTime().toUnix()
  
  index.db.exec(sql"""
    INSERT OR REPLACE INTO cache_entries
    (cache_key, cas_id, timestamp, hit_count, last_access)
    VALUES (?, ?, ?, COALESCE((SELECT hit_count FROM cache_entries WHERE cache_key = ?), 0), ?)
  """, cacheKey, casId, now, cacheKey, now)

proc delete*(index: PersistentCacheIndex, cacheKey: string): bool =
  ## Delete cache entry.
  ##
  ## **Parameters:**
  ## - cacheKey: Cache key to delete
  ##
  ## **Returns:** true if entry was deleted, false if not found
  
  if not index.enabled:
    return false
  
  let rowsBefore = index.db.getValue(sql"SELECT COUNT(*) FROM cache_entries").parseInt
  
  index.db.exec(sql"""
    DELETE FROM cache_entries WHERE cache_key = ?
  """, cacheKey)
  
  let rowsAfter = index.db.getValue(sql"SELECT COUNT(*) FROM cache_entries").parseInt
  
  return rowsBefore > rowsAfter

proc clear*(index: PersistentCacheIndex) =
  ## Clear all cache entries.
  
  if not index.enabled:
    return
  
  index.db.exec(sql"DELETE FROM cache_entries")

proc prune*(index: PersistentCacheIndex, olderThan: Duration): int =
  ## Prune cache entries older than specified duration.
  ##
  ## **Parameters:**
  ## - olderThan: Delete entries older than this duration
  ##
  ## **Returns:** Number of entries deleted
  ##
  ## **Example:**
  ## ```nim
  ## let deleted = index.prune(initDuration(days = 30))
  ## echo "Deleted ", deleted, " entries older than 30 days"
  ## ```
  
  if not index.enabled:
    return 0
  
  let cutoff = (now() - olderThan).toTime().toUnix()
  
  let rowsBefore = index.db.getValue(sql"SELECT COUNT(*) FROM cache_entries").parseInt
  
  index.db.exec(sql"""
    DELETE FROM cache_entries WHERE last_access < ?
  """, cutoff)
  
  let rowsAfter = index.db.getValue(sql"SELECT COUNT(*) FROM cache_entries").parseInt
  
  return rowsBefore - rowsAfter

proc pruneLRU*(index: PersistentCacheIndex, keepCount: int): int =
  ## Prune least recently used entries, keeping only specified count.
  ##
  ## **Parameters:**
  ## - keepCount: Number of entries to keep
  ##
  ## **Returns:** Number of entries deleted
  
  if not index.enabled:
    return 0
  
  let rowsBefore = index.db.getValue(sql"SELECT COUNT(*) FROM cache_entries").parseInt
  
  if rowsBefore <= keepCount:
    return 0
  
  index.db.exec(sql"""
    DELETE FROM cache_entries
    WHERE cache_key NOT IN (
      SELECT cache_key FROM cache_entries
      ORDER BY last_access DESC
      LIMIT ?
    )
  """, keepCount)
  
  let rowsAfter = index.db.getValue(sql"SELECT COUNT(*) FROM cache_entries").parseInt
  
  return rowsBefore - rowsAfter

# ============================================================================
# Statistics and Inspection
# ============================================================================

proc getStats*(index: PersistentCacheIndex): CacheIndexStats =
  ## Get cache index statistics.
  ##
  ## **Returns:** Statistics including entry count, hits, age
  
  if not index.enabled:
    return CacheIndexStats()
  
  let totalEntries = index.db.getValue(sql"SELECT COUNT(*) FROM cache_entries").parseInt
  let totalHits = index.db.getValue(sql"SELECT SUM(hit_count) FROM cache_entries").parseInt
  
  let oldestTimestamp = index.db.getValue(sql"SELECT MIN(timestamp) FROM cache_entries")
  let newestTimestamp = index.db.getValue(sql"SELECT MAX(timestamp) FROM cache_entries")
  
  let oldestEntry = if oldestTimestamp.len > 0:
    fromUnix(oldestTimestamp.parseInt).local
  else:
    now()
  
  let newestEntry = if newestTimestamp.len > 0:
    fromUnix(newestTimestamp.parseInt).local
  else:
    now()
  
  # Get database file size
  let dbSize = 0'i64  # TODO: Get actual file size
  
  result = CacheIndexStats(
    totalEntries: totalEntries,
    totalHits: totalHits,
    oldestEntry: oldestEntry,
    newestEntry: newestEntry,
    dbSize: dbSize
  )

proc listEntries*(index: PersistentCacheIndex, limit: int = 100): seq[CacheEntry] =
  ## List cache entries (most recently accessed first).
  ##
  ## **Parameters:**
  ## - limit: Maximum number of entries to return
  ##
  ## **Returns:** Sequence of cache entries
  
  if not index.enabled:
    return @[]
  
  result = @[]
  
  for row in index.db.fastRows(sql"""
    SELECT cache_key, cas_id, timestamp, hit_count, last_access
    FROM cache_entries
    ORDER BY last_access DESC
    LIMIT ?
  """, limit):
    result.add(CacheEntry(
      cacheKey: row[0],
      casId: row[1],
      timestamp: fromUnix(row[2].parseInt).local,
      hitCount: row[3].parseInt,
      lastAccess: fromUnix(row[4].parseInt).local
    ))

proc getMostUsed*(index: PersistentCacheIndex, limit: int = 10): seq[CacheEntry] =
  ## Get most frequently used cache entries.
  ##
  ## **Parameters:**
  ## - limit: Maximum number of entries to return
  ##
  ## **Returns:** Sequence of cache entries sorted by hit count
  
  if not index.enabled:
    return @[]
  
  result = @[]
  
  for row in index.db.fastRows(sql"""
    SELECT cache_key, cas_id, timestamp, hit_count, last_access
    FROM cache_entries
    ORDER BY hit_count DESC
    LIMIT ?
  """, limit):
    result.add(CacheEntry(
      cacheKey: row[0],
      casId: row[1],
      timestamp: fromUnix(row[2].parseInt).local,
      hitCount: row[3].parseInt,
      lastAccess: fromUnix(row[4].parseInt).local
    ))

# ============================================================================
# Maintenance Operations
# ============================================================================

proc vacuum*(index: PersistentCacheIndex) =
  ## Vacuum database to reclaim space.
  
  if not index.enabled:
    return
  
  index.db.exec(sql"VACUUM")

proc analyze*(index: PersistentCacheIndex) =
  ## Analyze database for query optimization.
  
  if not index.enabled:
    return
  
  index.db.exec(sql"ANALYZE")

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

proc `$`*(stats: CacheIndexStats): string =
  ## String representation of cache index statistics.
  
  result = "CacheIndexStats(\n"
  result &= "  total entries: " & $stats.totalEntries & "\n"
  result &= "  total hits: " & $stats.totalHits & "\n"
  result &= "  oldest entry: " & $stats.oldestEntry.format("yyyy-MM-dd HH:mm:ss") & "\n"
  result &= "  newest entry: " & $stats.newestEntry.format("yyyy-MM-dd HH:mm:ss") & "\n"
  result &= "  db size: " & $(stats.dbSize div 1024) & " KB\n"
  result &= ")"

proc `$`*(entry: CacheEntry): string =
  ## String representation of cache entry.
  
  result = "CacheEntry(\n"
  result &= "  cache key: " & entry.cacheKey[0..min(31, entry.cacheKey.len-1)] & "...\n"
  result &= "  CAS ID: " & entry.casId[0..min(31, entry.casId.len-1)] & "...\n"
  result &= "  timestamp: " & entry.timestamp.format("yyyy-MM-dd HH:mm:ss") & "\n"
  result &= "  hit count: " & $entry.hitCount & "\n"
  result &= "  last access: " & entry.lastAccess.format("yyyy-MM-dd HH:mm:ss") & "\n"
  result &= ")"

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

when isMainModule:
  import unittest
  import os
  
  suite "Persistent Cache Index":
    setup:
      let testDb = "/tmp/test-cache-" & $now().toTime().toUnix() & ".db"
    
    teardown:
      if fileExists(testDb):
        removeFile(testDb)
    
    test "Create index":
      let index = newPersistentCacheIndex(testDb)
      check index.enabled
      index.close()
    
    test "Put and get entry":
      let index = newPersistentCacheIndex(testDb)
      
      index.put("key1", "cas-id-123")
      
      let result = index.get("key1")
      check result.isSome
      check result.get == "cas-id-123"
      
      index.close()
    
    test "Get non-existent entry":
      let index = newPersistentCacheIndex(testDb)
      
      let result = index.get("missing")
      check result.isNone
      
      index.close()
    
    test "Update existing entry":
      let index = newPersistentCacheIndex(testDb)
      
      index.put("key1", "cas-id-123")
      index.put("key1", "cas-id-456")
      
      let result = index.get("key1")
      check result.get == "cas-id-456"
      
      index.close()
    
    test "Delete entry":
      let index = newPersistentCacheIndex(testDb)
      
      index.put("key1", "cas-id-123")
      check index.delete("key1")
      check index.get("key1").isNone
      
      index.close()
    
    test "Clear all entries":
      let index = newPersistentCacheIndex(testDb)
      
      index.put("key1", "cas-id-123")
      index.put("key2", "cas-id-456")
      
      index.clear()
      
      check index.get("key1").isNone
      check index.get("key2").isNone
      
      index.close()
    
    test "Get statistics":
      let index = newPersistentCacheIndex(testDb)
      
      index.put("key1", "cas-id-123")
      index.put("key2", "cas-id-456")
      
      let stats = index.getStats()
      check stats.totalEntries == 2
      
      index.close()
