## Build Synthesis Module
##
## This module implements deterministic build synthesis for the NIP dependency resolver.
## It takes unified variant profiles and synthesizes reproducible builds with deterministic
## hashing for content-addressable storage.
##
## Philosophy:
## - Build synthesis is the bridge between variant unification and CAS storage
## - Every build has a deterministic hash based on its configuration
## - Same variant profile + source = same build hash (reproducibility guarantee)
## - Build hashes are xxh3-128 for performance (non-cryptographic)

import std/[tables, strutils, times, options, sequtils, algorithm, os]
import ../xxhash  # For xxh3-128 hashing
import ./variant_types

type
  # Build configuration for synthesis
  BuildConfig* = object
    packageName*: string
    packageVersion*: string
    variantProfile*: VariantProfile
    sourceHash*: string           # Hash of source code
    compilerVersion*: string      # Compiler version used
    compilerFlags*: seq[string]   # Compiler flags
    configureFlags*: seq[string]  # Configure flags
    targetArchitecture*: string   # Target architecture
    libc*: string                 # libc type (musl, glibc)
    allocator*: string            # Memory allocator (jemalloc, tcmalloc)
    timestamp*: times.Time        # Build timestamp

  # Result of build synthesis
  BuildSynthesisResult* = object
    buildHash*: string            # xxh3-128 hash of build
    casID*: string                # CAS identifier (same as buildHash)
    buildConfig*: BuildConfig
    timestamp*: times.Time

  # Build synthesis error
  BuildSynthesisError* = object of CatchableError
    reason*: string

# Constructor for BuildConfig
proc newBuildConfig*(
  packageName: string,
  packageVersion: string,
  variantProfile: VariantProfile,
  sourceHash: string = "",
  compilerVersion: string = "gcc-13.2.0",
  compilerFlags: seq[string] = @["-O2", "-march=native"],
  configureFlags: seq[string] = @[],
  targetArchitecture: string = "x86_64",
  libc: string = "musl",
  allocator: string = "jemalloc"
): BuildConfig =
  result.packageName = packageName
  result.packageVersion = packageVersion
  result.variantProfile = variantProfile
  result.sourceHash = sourceHash
  result.compilerVersion = compilerVersion
  result.compilerFlags = compilerFlags
  result.configureFlags = configureFlags
  result.targetArchitecture = targetArchitecture
  result.libc = libc
  result.allocator = allocator
  result.timestamp = getTime()

# Calculate canonical representation for build hash
proc toCanonical*(config: BuildConfig): string =
  ## Convert build config to canonical string for deterministic hashing
  ## Format: package|version|variant_hash|source_hash|compiler|flags|arch|libc|allocator
  ##
  ## This ensures:
  ## - Same configuration always produces same hash
  ## - Different configurations produce different hashes
  ## - Hash is deterministic across builds and machines
  
  var parts: seq[string] = @[]
  
  # Package identification
  parts.add(config.packageName)
  parts.add(config.packageVersion)
  
  # Variant profile (already canonical)
  parts.add(config.variantProfile.toCanonical())
  
  # Source integrity
  parts.add(config.sourceHash)
  
  # Compiler configuration (sorted for determinism)
  parts.add(config.compilerVersion)
  parts.add(config.compilerFlags.sorted().join(","))
  
  # Build configuration (sorted for determinism)
  parts.add(config.configureFlags.sorted().join(","))
  
  # Target environment
  parts.add(config.targetArchitecture)
  parts.add(config.libc)
  parts.add(config.allocator)
  
  # Join with | separator
  result = parts.join("|")

# Calculate build hash using xxh3-128
proc calculateBuildHash*(config: BuildConfig): string =
  ## Calculate deterministic xxh3-128 hash for build configuration
  ##
  ## This hash serves as:
  ## - Unique identifier for the build
  ## - CAS identifier for storage
  ## - Reproducibility guarantee (same config = same hash)
  
  let canonical = config.toCanonical()
  let hashValue = calculateXXH3(canonical)
  result = $hashValue  # XXH3Hash already includes "xxh3-" prefix

# Synthesize a build from variant profile
proc synthesizeBuild*(
  packageName: string,
  packageVersion: string,
  variantProfile: VariantProfile,
  sourceHash: string = "",
  compilerVersion: string = "gcc-13.2.0",
  compilerFlags: seq[string] = @["-O2", "-march=native"],
  configureFlags: seq[string] = @[],
  targetArchitecture: string = "x86_64",
  libc: string = "musl",
  allocator: string = "jemalloc"
): BuildSynthesisResult =
  ## Synthesize a build from a unified variant profile
  ##
  ## This function:
  ## 1. Creates a build configuration from the variant profile
  ## 2. Calculates a deterministic build hash
  ## 3. Returns the build hash as CAS identifier
  ##
  ## The build hash is deterministic: same inputs always produce same hash
  
  # Create build configuration
  let config = newBuildConfig(
    packageName = packageName,
    packageVersion = packageVersion,
    variantProfile = variantProfile,
    sourceHash = sourceHash,
    compilerVersion = compilerVersion,
    compilerFlags = compilerFlags,
    configureFlags = configureFlags,
    targetArchitecture = targetArchitecture,
    libc = libc,
    allocator = allocator
  )
  
  # Calculate deterministic build hash
  let buildHash = calculateBuildHash(config)
  
  # Return synthesis result
  result = BuildSynthesisResult(
    buildHash: buildHash,
    casID: buildHash,  # CAS ID is the build hash
    buildConfig: config,
    timestamp: getTime()
  )

# Store synthesized build in CAS
proc storeBuildInCAS*(
  buildResult: BuildSynthesisResult,
  casRoot: string,
  buildMetadata: string = ""
): string =
  ## Store synthesized build in CAS and return CAS ID
  ##
  ## This function:
  ## 1. Serializes the build configuration
  ## 2. Stores it in the CAS using the build hash as identifier
  ## 3. Returns the CAS ID for later retrieval
  ##
  ## The CAS ensures:
  ## - Content-addressed storage (hash = identifier)
  ## - Deduplication (same build = same hash = single storage)
  ## - Integrity verification (hash matches content)
  
  # Serialize build configuration
  var serialized = ""
  serialized.add("package: " & buildResult.buildConfig.packageName & "\n")
  serialized.add("version: " & buildResult.buildConfig.packageVersion & "\n")
  serialized.add("variant: " & buildResult.buildConfig.variantProfile.toCanonical() & "\n")
  serialized.add("source_hash: " & buildResult.buildConfig.sourceHash & "\n")
  serialized.add("compiler: " & buildResult.buildConfig.compilerVersion & "\n")
  serialized.add("compiler_flags: " & buildResult.buildConfig.compilerFlags.sorted().join(",") & "\n")
  serialized.add("configure_flags: " & buildResult.buildConfig.configureFlags.sorted().join(",") & "\n")
  serialized.add("target_arch: " & buildResult.buildConfig.targetArchitecture & "\n")
  serialized.add("libc: " & buildResult.buildConfig.libc & "\n")
  serialized.add("allocator: " & buildResult.buildConfig.allocator & "\n")
  serialized.add("build_hash: " & buildResult.buildHash & "\n")
  
  if buildMetadata != "":
    serialized.add("metadata: " & buildMetadata & "\n")
  
  # Create directory structure for CAS storage
  let shardPath = buildResult.casID[0..3]  # Use first 4 chars for sharding
  let fullShardPath = casRoot / shardPath
  createDir(fullShardPath)
  
  # Store the serialized build
  let objectPath = fullShardPath / buildResult.casID
  writeFile(objectPath, serialized)
  
  # Return CAS ID (which is the build hash)
  result = buildResult.casID

# Retrieve build from CAS
proc retrieveBuildFromCAS*(
  casID: string,
  casRoot: string
): BuildSynthesisResult =
  ## Retrieve a synthesized build from CAS by its ID
  ##
  ## This function:
  ## 1. Retrieves the build metadata from CAS
  ## 2. Verifies the hash matches the CAS ID
  ## 3. Reconstructs the build configuration
  
  # Construct path to retrieve from CAS
  let shardPath = casID[0..3]  # Use first 4 chars for sharding
  let objectPath = casRoot / shardPath / casID
  
  if not fileExists(objectPath):
    raise newException(BuildSynthesisError, "Build not found in CAS: " & casID)
  
  # Retrieve from CAS
  let data = readFile(objectPath)
  
  # Parse the serialized data (simplified parsing)
  var config = BuildConfig()
  var buildHash = ""
  var variantCanonical = ""
  var compilerFlagsStr = ""
  var configureFlagsStr = ""
  
  for line in data.split("\n"):
    if line.startsWith("package: "):
      config.packageName = line[9..^1]
    elif line.startsWith("version: "):
      config.packageVersion = line[9..^1]
    elif line.startsWith("variant: "):
      variantCanonical = line[9..^1]
    elif line.startsWith("source_hash: "):
      config.sourceHash = line[13..^1]
    elif line.startsWith("compiler: "):
      config.compilerVersion = line[10..^1]
    elif line.startsWith("compiler_flags: "):
      compilerFlagsStr = line[16..^1]
    elif line.startsWith("configure_flags: "):
      configureFlagsStr = line[17..^1]
    elif line.startsWith("target_arch: "):
      config.targetArchitecture = line[13..^1]
    elif line.startsWith("libc: "):
      config.libc = line[6..^1]
    elif line.startsWith("allocator: "):
      config.allocator = line[11..^1]
    elif line.startsWith("build_hash: "):
      buildHash = line[12..^1]
  
  # Reconstruct compiler and configure flags
  if compilerFlagsStr != "":
    config.compilerFlags = compilerFlagsStr.split(",")
  if configureFlagsStr != "":
    config.configureFlags = configureFlagsStr.split(",")
  
  # Reconstruct variant profile from canonical representation
  var reconstructedProfile = newVariantProfile()
  # Parse the canonical representation: domain1:flag1,flag2|domain2:flag3
  for domainPart in variantCanonical.split("|"):
    if domainPart.contains(":"):
      let parts = domainPart.split(":")
      let domainName = parts[0]
      let flagsStr = parts[1]
      for flag in flagsStr.split(","):
        if flag != "":
          reconstructedProfile.addFlag(domainName, flag)
  reconstructedProfile.calculateHash()
  config.variantProfile = reconstructedProfile
  
  # Verify hash matches
  let calculatedHash = calculateBuildHash(config)
  if calculatedHash != buildHash:
    raise newException(BuildSynthesisError, 
      "Build hash mismatch: expected " & buildHash & ", got " & calculatedHash)
  
  result = BuildSynthesisResult(
    buildHash: buildHash,
    casID: casID,
    buildConfig: config,
    timestamp: getTime()
  )

# Verify build hash matches configuration
proc verifyBuildHash*(
  buildHash: string,
  config: BuildConfig
): bool =
  ## Verify that a build hash matches its configuration
  ##
  ## This ensures:
  ## - Build integrity (hash matches configuration)
  ## - Reproducibility (same config = same hash)
  ## - No tampering (hash mismatch = configuration changed)
  
  let calculatedHash = calculateBuildHash(config)
  result = buildHash == calculatedHash

# Check if two builds are identical
proc isBuildIdentical*(
  build1: BuildSynthesisResult,
  build2: BuildSynthesisResult
): bool =
  ## Check if two builds are identical
  ##
  ## Two builds are identical if:
  ## - They have the same build hash
  ## - Their configurations produce the same hash
  
  result = build1.buildHash == build2.buildHash and
           build1.buildHash == calculateBuildHash(build2.buildConfig)

# String representation for display
proc `$`*(bsr: BuildSynthesisResult): string =
  ## Human-readable string representation
  
  result = "BuildSynthesisResult(\n" &
    "  package: " & bsr.buildConfig.packageName & "\n" &
    "  version: " & bsr.buildConfig.packageVersion & "\n" &
    "  variant: " & bsr.buildConfig.variantProfile.toCanonical() & "\n" &
    "  build_hash: " & bsr.buildHash & "\n" &
    "  cas_id: " & bsr.casID & "\n" &
    ")"
