## Variant System Types
##
## This module defines the core types for NIP's variant system, which enables
## the "Paradoxical Fusion" - synthesizing single builds that satisfy multiple
## conflicting demands through dialectical synthesis.
##
## Philosophy:
## - Every variant profile has a unique deterministic hash
## - Exclusive domains enforce mutual exclusion (e.g., init=systemd XOR init=dinit)
## - Non-exclusive domains accumulate flags (e.g., +wayland AND +vulkan)
## - Variant unification is the heart of the Paradox Engine

import std/[tables, sets, strutils, algorithm, sequtils]
import ../xxhash  # For xxh3-128 hashing (placeholder for xxh4)
# import ../cas     # For CAS.write() integration (future use)

type
  # Domain exclusivity determines merging behavior
  DomainExclusivity* = enum
    Exclusive,      ## Only one value allowed (e.g., init system)
    NonExclusive    ## Multiple values can accumulate (e.g., features)

  # A domain groups related variant flags
  VariantDomain* = object
    name*: string
    exclusivity*: DomainExclusivity
    flags*: HashSet[string]

  # Complete variant profile for a package
  VariantProfile* = object
    domains*: Table[string, VariantDomain]
    hash*: string  ## xxh4-128 deterministic hash

  # A demand for a specific variant from a parent package
  VariantDemand* = object
    packageName*: string
    variantProfile*: VariantProfile
    optional*: bool

  # Result of unification attempt
  UnificationKind* = enum
    Unified,    ## Successfully merged all demands
    Conflict    ## Incompatible demands detected

  UnificationResult* = object
    case kind*: UnificationKind
    of Unified:
      profile*: VariantProfile
    of Conflict:
      conflictingDemands*: seq[VariantDemand]
      conflictingDomain*: string
      reason*: string

# Constructor for VariantDomain
proc newVariantDomain*(name: string, exclusivity: DomainExclusivity): VariantDomain =
  result.name = name
  result.exclusivity = exclusivity
  result.flags = initHashSet[string]()

# Constructor for VariantProfile
proc newVariantProfile*(): VariantProfile =
  result.domains = initTable[string, VariantDomain]()
  result.hash = ""

# Add a domain to a variant profile
proc addDomain*(profile: var VariantProfile, domain: VariantDomain) =
  profile.domains[domain.name] = domain

# Add a flag to a domain in a variant profile
proc addFlag*(profile: var VariantProfile, domainName: string, flag: string) =
  if not profile.domains.hasKey(domainName):
    # Create non-exclusive domain by default
    profile.domains[domainName] = newVariantDomain(domainName, NonExclusive)
  
  profile.domains[domainName].flags.incl(flag)

# Check if profile has a specific domain
proc hasDomain*(profile: VariantProfile, domainName: string): bool =
  profile.domains.hasKey(domainName)

# Get a domain from profile
proc getDomain*(profile: VariantProfile, domainName: string): VariantDomain =
  if not profile.hasDomain(domainName):
    raise newException(KeyError, "Domain not found: " & domainName)
  profile.domains[domainName]

# Check if two domains are compatible for merging
proc isCompatible*(domain1, domain2: VariantDomain): bool =
  ## Check if two domains can be merged
  ## Exclusive domains must have identical flags
  ## Non-exclusive domains can always merge (flags accumulate)
  
  if domain1.exclusivity == Exclusive or domain2.exclusivity == Exclusive:
    # Exclusive domains must match exactly
    return domain1.flags == domain2.flags
  else:
    # Non-exclusive domains can always merge
    return true

# Merge two domains (assumes compatibility check passed)
proc merge*(domain1: var VariantDomain, domain2: VariantDomain) =
  ## Merge domain2 into domain1
  ## For exclusive domains: flags must already match (checked by isCompatible)
  ## For non-exclusive domains: accumulate all flags
  
  if domain1.exclusivity == Exclusive and domain2.exclusivity == Exclusive:
    # Exclusive domains must match exactly - already verified by isCompatible
    # No merging needed, flags are identical
    discard
  else:
    # Non-exclusive: accumulate all flags
    for flag in domain2.flags:
      domain1.flags.incl(flag)

# Canonical string representation for hashing
proc toCanonical*(profile: VariantProfile): string =
  ## Convert variant profile to canonical string representation
  ## Used for deterministic hash calculation
  ## Format: domain1:flag1,flag2|domain2:flag3,flag4
  
  var parts: seq[string] = @[]
  
  # Sort domains alphabetically for determinism
  let sortedDomains = toSeq(profile.domains.keys).sorted()
  
  for domainName in sortedDomains:
    let domain = profile.domains[domainName]
    
    # Sort flags alphabetically for determinism
    let sortedFlags = toSeq(domain.flags).sorted()
    
    # Format: domain:flag1,flag2
    let flagStr = sortedFlags.join(",")
    parts.add(domainName & ":" & flagStr)
  
  # Join with | separator
  result = parts.join("|")

# String representation for display
proc `$`*(profile: VariantProfile): string =
  ## Human-readable string representation
  
  if profile.domains.len == 0:
    return "VariantProfile(empty)"
  
  var parts: seq[string] = @[]
  
  for domainName, domain in profile.domains:
    let flags = toSeq(domain.flags).sorted().join(",")
    let exclusivity = if domain.exclusivity == Exclusive: "!" else: ""
    parts.add(domainName & exclusivity & ":" & flags)
  
  result = "VariantProfile(" & parts.join(" ") & ")"

proc `$`*(demand: VariantDemand): string =
  ## Human-readable string representation of demand
  
  let optional = if demand.optional: " [optional]" else: ""
  result = demand.packageName & optional & " " & $demand.variantProfile

proc `$`*(unificationResult: UnificationResult): string =
  ## Human-readable string representation of unification result
  
  case unificationResult.kind:
  of Unified:
    result = "Unified: " & $unificationResult.profile
  of Conflict:
    result = "Conflict in domain '" & unificationResult.conflictingDomain & "': " & unificationResult.reason

# Equality comparison for variant profiles
proc `==`*(a, b: VariantProfile): bool =
  ## Two profiles are equal if they have the same canonical representation
  ## This ensures deterministic comparison
  
  a.toCanonical() == b.toCanonical()

# Equality comparison for variant domains
proc `==`*(a, b: VariantDomain): bool =
  ## Two domains are equal if they have the same name, exclusivity, and flags
  
  a.name == b.name and
  a.exclusivity == b.exclusivity and
  a.flags == b.flags

# Exception type for variant conflicts
type
  VariantConflict* = object of CatchableError
    domain*: string

# Calculate hash for variant profile (lazy + cached)
proc calculateHash*(profile: var VariantProfile) =
  ## Calculate xxh3-128 hash for variant profile (placeholder for xxh4)
  ## Lazy evaluation: only compute if not already cached
  ## Invalidate cache by setting hash to ""
  
  if profile.hash != "":
    return  # Already computed
  
  let canonical = profile.toCanonical()
  let hashValue = calculateXXH3(canonical)
  profile.hash = $hashValue

# Merge two variant profiles
proc merge*(a: var VariantProfile, b: VariantProfile) =
  ## Merge profile b into profile a
  ## Raises VariantConflict if domains are incompatible
  
  for domainName, domainB in b.domains:
    if not a.domains.hasKey(domainName):
      # New domain - just add it
      a.domains[domainName] = domainB
    else:
      # Existing domain - check compatibility and merge
      var domainA = a.domains[domainName]
      
      if not isCompatible(domainA, domainB):
        var exc = newException(VariantConflict, 
          "Cannot merge incompatible domains: " & domainName)
        exc.domain = domainName
        raise exc
      
      domainA.merge(domainB)
      a.domains[domainName] = domainA
  
  # Invalidate hash cache after merge
  a.hash = ""

# The unification function - heart of the Paradox Engine
proc unify*(demands: seq[VariantDemand]): UnificationResult =
  ## Unify multiple variant demands into a single profile
  ## This is the core of the Paradoxical Fusion algorithm
  ## Returns Unified with merged profile, or Conflict with details
  
  if demands.len == 0:
    return UnificationResult(kind: Unified, profile: newVariantProfile())
  
  if demands.len == 1:
    return UnificationResult(kind: Unified, profile: demands[0].variantProfile)
  
  # Start with first demand
  var unified = demands[0].variantProfile
  
  # Merge each subsequent demand
  for i in 1..<demands.len:
    try:
      unified.merge(demands[i].variantProfile)
    except VariantConflict as e:
      return UnificationResult(
        kind: Conflict,
        conflictingDemands: demands,
        conflictingDomain: e.domain,
        reason: e.msg
      )
  
  # Calculate final hash
  unified.calculateHash()
  
  return UnificationResult(kind: Unified, profile: unified)
