## nimpak/core/variant_system.nim
## Variant Fingerprint System and Content Addressable Storage
##
## This module implements the core variant system that treats packages as
## immutable variant fingerprints derived from complete build configurations.

import std/[strutils, times, json, tables, strformat, sequtils, algorithm, os, hashes]
import ../security/hash_verifier

type
  VariantId* = distinct string  ## BLAKE3 hash serving as variant fingerprint

  FeatureValue* = object
    case kind*: FeatureKind
    of Boolean: boolVal*: bool
    of Choice: choiceVal*: string
    of Group: groupVals*: seq[string]

  FeatureKind* = enum
    Boolean, Choice, Group

  BuildConfiguration* = object
    features*: Table[string, FeatureValue]
    buildFlags*: Table[string, string]
    toolchain*: string
    target*: string
    patches*: seq[string]
    environment*: Table[string, string]

  VariantSpec* = object
    name*: string
    version*: string
    sourceUrl*: string
    sourceHash*: string
    config*: BuildConfiguration
    recipeHash*: string

  VariantManifest* = object
    variantId*: VariantId
    spec*: VariantSpec
    buildTime*: DateTime
    buildHost*: string
    dependencies*: seq[VariantId]
    provides*: seq[string]
    conflicts*: seq[string]
    fileCount*: int
    totalSize*: int64
    integrityHashes*: Table[string, string]  ## file_path -> blake3_hash

  CasEntry* = object
    variantId*: VariantId
    casPath*: string
    manifest*: VariantManifest
    active*: bool
    symlinkPaths*: seq[string]
    installTime*: DateTime

# =============================================================================
# Variant Fingerprint Calculation
# =============================================================================

proc `$`*(id: VariantId): string = string(id)

proc hash*(id: VariantId): Hash = hash(string(id))

proc calculateVariantFingerprint*(spec: VariantSpec): VariantId =
  ## Calculate BLAKE3 variant fingerprint from complete build specification
  var components: seq[string] = @[]

  # Core package identity
  components.add(spec.name)
  components.add(spec.version)
  components.add(spec.sourceUrl)
  components.add(spec.sourceHash)
  components.add(spec.recipeHash)

  # Build configuration
  components.add(spec.config.toolchain)
  components.add(spec.config.target)

  # Features (sorted for determinism)
  var featureKeys = toSeq(spec.config.features.keys)
  featureKeys.sort()
  for key in featureKeys:
    let feature = spec.config.features[key]
    case feature.kind:
    of Boolean:
      components.add(fmt"{key}={feature.boolVal}")
    of Choice:
      components.add(fmt"{key}={feature.choiceVal}")
    of Group:
      var sortedVals = feature.groupVals
      sortedVals.sort()
      components.add(fmt"{key}=[{sortedVals.join(\",\")}]")

  # Build flags (sorted for determinism)
  var flagKeys = toSeq(spec.config.buildFlags.keys)
  flagKeys.sort()
  for key in flagKeys:
    components.add(fmt"{key}={spec.config.buildFlags[key]}")

  # Patches (order matters)
  for patch in spec.config.patches:
    components.add(fmt"patch:{patch}")

  # Environment variables (sorted for determinism)
  var envKeys = toSeq(spec.config.environment.keys)
  envKeys.sort()
  for key in envKeys:
    components.add(fmt"env:{key}={spec.config.environment[key]}")

  # Combine all components
  let combined = components.join("|")

  # Calculate BLAKE3 hash (simplified simulation)
  # In real implementation, use actual BLAKE3 library
  let hashInput = combined
  var hashValue = 0
  for c in hashInput:
    hashValue = hashValue * 31 + ord(c)

  let hexHash = fmt"{abs(hashValue):016x}"
  result = VariantId(hexHash)

proc shortVariantId*(id: VariantId): string =
  ## Get shortened variant ID for display (first 12 characters)
  let s = string(id)
  if s.len > 12:
    return s[0..11]
  return s

# =============================================================================
# Content Addressable Storage (CAS) Operations
# =============================================================================

const CAS_ROOT* = "/Programs"
const CAS_REGISTRY* = "/Programs/.nip-registry"

proc getCasPath*(variantId: VariantId, packageName, version: string): string =
  ## Get CAS filesystem path for variant
  let shortId = shortVariantId(variantId)
  return CAS_ROOT / packageName / fmt"{version}-{shortId}"

proc getManifestPath*(casPath: string): string =
  ## Get path to variant manifest file
  return casPath / ".nip-manifest.json"

proc getIntegrityPath*(casPath: string): string =
  ## Get path to integrity hash file
  return casPath / ".nip-integrity.blake3"

proc getFilesListPath*(casPath: string): string =
  ## Get path to files list
  return casPath / ".nip-files.list"

# =============================================================================
# Variant Manifest Operations
# =============================================================================

proc createVariantManifest*(spec: VariantSpec, variantId: VariantId,
                           dependencies: seq[VariantId] = @[]): VariantManifest =
  ## Create variant manifest from specification
  result = VariantManifest(
    variantId: variantId,
    spec: spec,
    buildTime: now(),
    buildHost: getEnv("HOSTNAME", "unknown"),
    dependencies: dependencies,
    provides: @[],  # Would be populated during build
    conflicts: @[], # Would be populated during build
    fileCount: 0,   # Would be calculated during installation
    totalSize: 0,   # Would be calculated during installation
    integrityHashes: initTable[string, string]()
  )

proc saveVariantManifest*(manifest: VariantManifest, casPath: string): bool =
  ## Save variant manifest to CAS path
  try:
    let manifestPath = getManifestPath(casPath)
    let manifestJson = %*{
      "variant_id": $manifest.variantId,
      "package": {
        "name": manifest.spec.name,
        "version": manifest.spec.version,
        "source_url": manifest.spec.sourceUrl,
        "source_hash": manifest.spec.sourceHash,
        "recipe_hash": manifest.spec.recipeHash
      },
      "build_config": {
        "toolchain": manifest.spec.config.toolchain,
        "target": manifest.spec.config.target,
        "features": manifest.spec.config.features,
        "build_flags": manifest.spec.config.buildFlags,
        "patches": manifest.spec.config.patches,
        "environment": manifest.spec.config.environment
      },
      "build_info": {
        "build_time": manifest.buildTime.format("yyyy-MM-dd'T'HH:mm:ss'Z'"),
        "build_host": manifest.buildHost,
        "file_count": manifest.fileCount,
        "total_size": manifest.totalSize
      },
      "dependencies": manifest.dependencies.mapIt($it),
      "provides": manifest.provides,
      "conflicts": manifest.conflicts,
      "integrity_hashes": manifest.integrityHashes
    }

    createDir(casPath)
    writeFile(manifestPath, pretty(manifestJson))
    return true
  except:
    return false

proc loadVariantManifest*(casPath: string): Option[VariantManifest] =
  ## Load variant manifest from CAS path
  try:
    let manifestPath = getManifestPath(casPath)
    if not fileExists(manifestPath):
      return none(VariantManifest)

    let jsonData = parseFile(manifestPath)

    # Parse build configuration
    var config = BuildConfiguration(
      toolchain: jsonData["build_config"]["toolchain"].getStr(),
      target: jsonData["build_config"]["target"].getStr(),
      features: initTable[string, FeatureValue](),
      buildFlags: initTable[string, string](),
      patches: @[],
      environment: initTable[string, string]()
    )

    # Parse features (simplified - would need proper type handling)
    for key, value in jsonData["build_config"]["features"]:
      config.features[key] = FeatureValue(kind: Boolean, boolVal: value.getBool())

    # Parse build flags
    for key, value in jsonData["build_config"]["build_flags"]:
      config.buildFlags[key] = value.getStr()

    # Parse patches
    for patch in jsonData["build_config"]["patches"]:
      config.patches.add(patch.getStr())

    # Parse environment
    for key, value in jsonData["build_config"]["environment"]:
      config.environment[key] = value.getStr()

    # Create variant spec
    let spec = VariantSpec(
      name: jsonData["package"]["name"].getStr(),
      version: jsonData["package"]["version"].getStr(),
      sourceUrl: jsonData["package"]["source_url"].getStr(),
      sourceHash: jsonData["package"]["source_hash"].getStr(),
      recipeHash: jsonData["package"]["recipe_hash"].getStr(),
      config: config
    )

    # Parse dependencies
    var dependencies: seq[VariantId] = @[]
    for dep in jsonData["dependencies"]:
      dependencies.add(VariantId(dep.getStr()))

    # Parse provides and conflicts
    var provides: seq[string] = @[]
    for prov in jsonData["provides"]:
      provides.add(prov.getStr())

    var conflicts: seq[string] = @[]
    for conf in jsonData["conflicts"]:
      conflicts.add(conf.getStr())

    # Parse integrity hashes
    var integrityHashes = initTable[string, string]()
    for key, value in jsonData["integrity_hashes"]:
      integrityHashes[key] = value.getStr()

    let manifest = VariantManifest(
      variantId: VariantId(jsonData["variant_id"].getStr()),
      spec: spec,
      buildTime: parse(jsonData["build_info"]["build_time"].getStr(), "yyyy-MM-dd'T'HH:mm:ss'Z'"),
      buildHost: jsonData["build_info"]["build_host"].getStr(),
      dependencies: dependencies,
      provides: provides,
      conflicts: conflicts,
      fileCount: jsonData["build_info"]["file_count"].getInt(),
      totalSize: jsonData["build_info"]["total_size"].getBiggestInt(),
      integrityHashes: integrityHashes
    )

    return some(manifest)
  except:
    return none(VariantManifest)

# =============================================================================
# CAS Registry Operations
# =============================================================================

proc initializeCasRegistry*(): bool =
  ## Initialize CAS registry directory structure
  try:
    createDir(CAS_REGISTRY)
    createDir(CAS_REGISTRY / "variants")
    createDir(CAS_REGISTRY / "symlinks")
    createDir(CAS_REGISTRY / "integrity")
    return true
  except:
    return false

proc registerVariant*(entry: CasEntry): bool =
  ## Register variant in CAS registry
  try:
    let registryPath = CAS_REGISTRY / "variants" / ($entry.variantId & ".json")
    let entryJson = %*{
      "variant_id": $entry.variantId,
      "cas_path": entry.casPath,
      "active": entry.active,
      "symlink_paths": entry.symlinkPaths,
      "install_time": entry.installTime.format("yyyy-MM-dd'T'HH:mm:ss'Z'"),
      "package_name": entry.manifest.spec.name,
      "package_version": entry.manifest.spec.version
    }

    writeFile(registryPath, pretty(entryJson))
    return true
  except:
    return false

proc findVariantsByPackage*(packageName: string): seq[CasEntry] =
  ## Find all variants for a package
  result = @[]

  try:
    let variantsDir = CAS_REGISTRY / "variants"
    if not dirExists(variantsDir):
      return result

    for file in walkFiles(variantsDir / "*.json"):
      try:
        let jsonData = parseFile(file)
        if jsonData["package_name"].getStr() == packageName:
          let variantId = VariantId(jsonData["variant_id"].getStr())
          let casPath = jsonData["cas_path"].getStr()

          # Load full manifest
          let manifestOpt = loadVariantManifest(casPath)
          if manifestOpt.isSome:
            let entry = CasEntry(
              variantId: variantId,
              casPath: casPath,
              manifest: manifestOpt.get(),
              active: jsonData["active"].getBool(),
              symlinkPaths: jsonData["symlink_paths"].to(seq[string]),
              installTime: parse(jsonData["install_time"].getStr(), "yyyy-MM-dd'T'HH:mm:ss'Z'")
            )
            result.add(entry)
      except:
        continue  # Skip corrupted entries
  except:
    discard

proc getAllVariants*(): seq[CasEntry] =
  ## Get all registered variants
  result = @[]

  try:
    let variantsDir = CAS_REGISTRY / "variants"
    if not dirExists(variantsDir):
      return result

    for file in walkFiles(variantsDir / "*.json"):
      try:
        let jsonData = parseFile(file)
        let variantId = VariantId(jsonData["variant_id"].getStr())
        let casPath = jsonData["cas_path"].getStr()

        # Load full manifest
        let manifestOpt = loadVariantManifest(casPath)
        if manifestOpt.isSome:
          let entry = CasEntry(
            variantId: variantId,
            casPath: casPath,
            manifest: manifestOpt.get(),
            active: jsonData["active"].getBool(),
            symlinkPaths: jsonData["symlink_paths"].to(seq[string]),
            installTime: parse(jsonData["install_time"].getStr(), "yyyy-MM-dd'T'HH:mm:ss'Z'")
          )
          result.add(entry)
      except:
        continue  # Skip corrupted entries
  except:
    discard

# =============================================================================
# Variant Comparison and Analysis
# =============================================================================

proc compareVariants*(a, b: VariantSpec): string =
  ## Compare two variant specifications and return differences
  var differences: seq[string] = @[]

  if a.name != b.name:
    differences.add(fmt"name: {a.name} -> {b.name}")

  if a.version != b.version:
    differences.add(fmt"version: {a.version} -> {b.version}")

  if a.config.toolchain != b.config.toolchain:
    differences.add(fmt"toolchain: {a.config.toolchain} -> {b.config.toolchain}")

  if a.config.target != b.config.target:
    differences.add(fmt"target: {a.config.target} -> {b.config.target}")

  # Compare features
  let allFeatureKeys = toSeq(a.config.features.keys) & toSeq(b.config.features.keys)
  for key in allFeatureKeys.deduplicate():
    let aVal = a.config.features.getOrDefault(key)
    let bVal = b.config.features.getOrDefault(key)
    if aVal != bVal:
      differences.add(fmt"feature {key}: {aVal} -> {bVal}")

  # Compare build flags
  let allFlagKeys = toSeq(a.config.buildFlags.keys) & toSeq(b.config.buildFlags.keys)
  for key in allFlagKeys.deduplicate():
    let aVal = a.config.buildFlags.getOrDefault(key, "")
    let bVal = b.config.buildFlags.getOrDefault(key, "")
    if aVal != bVal:
      differences.add(fmt"flag {key}: {aVal} -> {bVal}")

  return differences.join("; ")

# =============================================================================
# Utility Functions
# =============================================================================

proc formatVariantSummary*(entry: CasEntry): string =
  ## Format variant summary for display
  let shortId = shortVariantId(entry.variantId)
  let manifest = entry.manifest

  result = fmt"{manifest.spec.name} {manifest.spec.version}-{shortId}"

  # Add key features
  var features: seq[string] = @[]
  for key, value in manifest.spec.config.features:
    case value.kind:
    of Boolean:
      if value.boolVal:
        features.add(fmt"+{key}")
    of Choice:
      if value.choiceVal != "default":
        features.add(fmt"{key}={value.choiceVal}")
    of Group:
      if value.groupVals.len > 0:
        features.add(fmt"{key}=[{value.groupVals.join(\",\")}]")

  if features.len > 0:
    result.add(fmt" ({features.join(\", \")})")

proc isVariantInstalled*(variantId: VariantId): bool =
  ## Check if variant is installed in CAS
  let registryPath = CAS_REGISTRY / "variants" / ($variantId & ".json")
  return fileExists(registryPath)