## Content-Addressable Storage (CAS) system for NimPak
##
## This module provides the core functionality for storing and retrieving
## content-addressed objects using BLAKE2b-512 hashing (with future support for BLAKE3).
## Objects are stored in a sharded directory structure for scalability.

import std/[os, strutils, times, posix]
import nimcrypto/hash
import nimcrypto/blake2
import nip/types

const
  DefaultHashAlgorithm* = "blake2b-512"  # Default hash algorithm
  ShardingLevels* = 2                    # Number of directory levels for sharding

type
  HashAlgorithm* = enum
    Blake2b512 = "blake2b-512"
    # Blake3 = "blake3"  # Will be added when available in Nimble

  CasObject* = object
    hash*: Multihash
    size*: int64
    compressed*: bool
    timestamp*: times.Time

proc calculateHash*(data: string, algorithm: HashAlgorithm = Blake2b512): Multihash =
  ## Calculate the hash of a string using the specified algorithm
  case algorithm:
  of Blake2b512:
    let digest = blake2_512.digest(data)
    var hexDigest = ""
    for b in digest.data:
      hexDigest.add(b.toHex(2).toLowerAscii())
    result = Multihash(hexDigest)

proc calculateFileHash*(path: string, algorithm: HashAlgorithm = Blake2b512): Multihash =
  ## Calculate the hash of a file using the specified algorithm
  if not fileExists(path):
    raise newException(IOError, "File not found: " & path)

  let data = readFile(path)
  result = calculateHash(data, algorithm)

proc getShardPath*(hash: Multihash, levels: int = ShardingLevels): string =
  ## Get the sharded path for a hash
  ## e.g., "ab/cd" for hash "abcdef123456..."
  let hashStr = string(hash)
  var parts: seq[string] = @[]

  for i in 0..<levels:
    if i*2+1 < hashStr.len:
      parts.add(hashStr[i*2..<i*2+2])
    else:
      break

  result = parts.join("/")

proc storeObject*(data: string, casRoot: string, compress: bool = true): CasObject =
  ## Store data in the CAS and return its hash
  let hash = calculateHash(data)
  let shardPath = getShardPath(hash)
  let fullShardPath = casRoot / shardPath

  # Create shard directories if they don't exist
  createDir(fullShardPath)

  # Store the object
  let objectPath = fullShardPath / string(hash)

  # TODO: Add zstd compression when needed
  writeFile(objectPath, data)

  result = CasObject(
    hash: hash,
    size: data.len.int64,
    compressed: compress,
    timestamp: getTime()
  )

proc retrieveObject*(hash: Multihash, casRoot: string): string =
  ## Retrieve an object from the CAS by its hash
  let shardPath = getShardPath(hash)
  let objectPath = casRoot / shardPath / string(hash)

  if not fileExists(objectPath):
    raise newException(IOError, "Object not found: " & string(hash))

  # TODO: Add zstd decompression when needed
  result = readFile(objectPath)

proc verifyObject*(hash: Multihash, data: string): bool =
  ## Verify that data matches its expected hash
  let calculatedHash = calculateHash(data)
  result = hash == calculatedHash

proc initCasManager*(userCasPath: string, systemCasPath: string): bool =
  ## Initialize the CAS manager by creating necessary directories
  try:
    createDir(userCasPath)
    setFilePermissions(userCasPath, {fpUserRead, fpUserWrite, fpUserExec})

    # Only create system CAS if running as root
    if posix.getuid() == 0:
      createDir(systemCasPath)
      setFilePermissions(systemCasPath, {fpUserRead, fpUserWrite, fpUserExec,
                                        fpGroupRead, fpGroupExec,
                                        fpOthersRead, fpOthersExec})

    result = true
    result = true
  except:
    result = false

# ============================================================================
# Reference Counting / Garbage Collection Support
# ============================================================================

proc getRefPath(casRoot, refType, hash, refId: string): string =
  ## Get path for a reference file: cas/refs/<type>/<hash>/<refId>
  result = casRoot / "refs" / refType / hash / refId

proc addReference*(casRoot: string, hash: Multihash, refType, refId: string) =
  ## Add a reference to a CAS object
  ## refType: "npk", "nip", "nexter"
  ## refId: Unique identifier for the reference (e.g. "package-name:version")
  let path = getRefPath(casRoot, refType, string(hash), refId)
  createDir(path.parentDir)
  writeFile(path, "") # Empty file acts as reference

proc removeReference*(casRoot: string, hash: Multihash, refType, refId: string) =
  ## Remove a reference to a CAS object
  let path = getRefPath(casRoot, refType, string(hash), refId)
  if fileExists(path):
    removeFile(path)
    # Try to remove parent dir (hash dir) if empty
    try:
      removeDir(path.parentDir)
    except:
      discard

proc hasReferences*(casRoot: string, hash: Multihash): bool =
  ## Check if a CAS object has any references
  # We need to check all refTypes
  let refsDir = casRoot / "refs"
  if not dirExists(refsDir): return false
  
  for kind, path in walkDir(refsDir):
    if kind == pcDir:
      let hashDir = path / string(hash)
      if dirExists(hashDir):
        # Check if directory is not empty
        for _ in walkDir(hashDir):
          return true
  return false

when isMainModule:
  # Simple test
  echo "Testing CAS functionality..."
  let testData = "Hello, NexusOS with Content-Addressable Storage!"
  let objHash = calculateHash(testData)
  echo "Hash: ", string(objHash)

  # Test sharding
  echo "Shard path: ", getShardPath(objHash)