## Dependency Graph Builder
##
## This module implements the graph builder that constructs dependency graphs
## from package demands. It recursively fetches dependencies, unifies variants,
## and builds the complete dependency graph.
##
## Philosophy:
## - Start with root demands (user requests)
## - Recursively fetch dependencies from package metadata
## - Group demands by package name for variant unification
## - Build complete graph with all dependencies resolved
## - Detect conflicts and cycles early
##
## The graph builder is the bridge between user requests and the solver.

import std/[tables, sets, options, sequtils]
import ./dependency_graph
import ./variant_types
import ./variant_hash
import ./source_adapter
import ../manifest_parser

type
  # Result of graph building operation
  GraphBuildResult* = object
    graph*: DependencyGraph
    conflicts*: seq[UnificationResult]
    warnings*: seq[string]

  # Error during graph building
  GraphBuildError* = object
    message*: string
    packageName*: string
    context*: string

  # Package metadata provider interface
  PackageProvider* = proc(packageName: string): Option[seq[VariantDemand]] {.closure.}

# Build dependency graph from root demands
proc buildDependencyGraph*(
  rootDemands: seq[VariantDemand],
  packageProvider: PackageProvider
): GraphBuildResult =
  ## Build a complete dependency graph from root package demands
  ## 
  ## This function:
  ## 1. Starts with root demands (user requests)
  ## 2. Recursively fetches dependencies for each package
  ## 3. Groups demands by package name
  ## 4. Unifies variant profiles for each package
  ## 5. Creates terms and edges in the dependency graph
  ## 
  ## Args:
  ##   rootDemands: Initial package demands from user
  ##   packageProvider: Function to get dependencies for a package
  ## 
  ## Returns:
  ##   GraphBuildResult with complete graph and any conflicts
  
  var graph = newDependencyGraph()
  var conflicts: seq[UnificationResult] = @[]
  var warnings: seq[string] = @[]
  var visited = initHashSet[string]()  # Track visited packages to avoid infinite recursion
  var allDemands = initTable[string, seq[VariantDemand]]()  # Group demands by package name
  
  # Recursive function to collect all demands
  proc collectDemands(demands: seq[VariantDemand]) =
    for demand in demands:
      # Skip if already processed this package
      if demand.packageName in visited:
        # Add to existing demands for unification
        if not allDemands.hasKey(demand.packageName):
          allDemands[demand.packageName] = @[]
        allDemands[demand.packageName].add(demand)
        continue
      
      visited.incl(demand.packageName)
      
      # Add this demand
      if not allDemands.hasKey(demand.packageName):
        allDemands[demand.packageName] = @[]
      allDemands[demand.packageName].add(demand)
      
      # Get dependencies for this package
      let dependencies = packageProvider(demand.packageName)
      if dependencies.isSome:
        # Recursively collect dependencies
        collectDemands(dependencies.get)
  
  # Start collection with root demands
  collectDemands(rootDemands)
  
  # Process each package: unify variants and create terms
  var packageTerms = initTable[string, PackageTermId]()
  
  for packageName, demands in allDemands.pairs:
    # Unify all variant demands for this package
    let unificationResult = unify(demands)
    
    case unificationResult.kind:
    of Unified:
      # Create unified term
      var profile = unificationResult.profile
      profile.calculateHash()
      let termId = createTermId(packageName, profile.hash)
      let term = PackageTerm(
        id: termId,
        packageName: packageName,
        version: SemanticVersion(major: 0, minor: 0, patch: 0), # Placeholder, needs real version resolution
        variantHash: profile.hash,
        variantProfile: profile,
        optional: demands.anyIt(it.optional),
        source: "unified"  # Will be determined by source selection
      )
      
      graph.addTerm(term)
      packageTerms[packageName] = termId
    
    of Conflict:
      # Record conflict for later handling
      conflicts.add(unificationResult)
      warnings.add("Variant conflict for package " & packageName & ": " & unificationResult.reason)
  
  # Create dependency edges
  for packageName, demands in allDemands.pairs:
    if not packageTerms.hasKey(packageName):
      continue  # Skip packages with conflicts
    
    let fromTermId = packageTerms[packageName]
    
    # Get dependencies for this package
    let dependencies = packageProvider(packageName)
    if dependencies.isSome:
      for depDemand in dependencies.get:
        if packageTerms.hasKey(depDemand.packageName):
          let toTermId = packageTerms[depDemand.packageName]
          
          # Determine dependency type
          let depType = if depDemand.optional: Optional else: Required
          
          let edge = DependencyEdge(
            fromTerm: fromTermId,
            toTerm: toTermId,
            dependencyType: depType,
            constraint: "" # TODO: Add constraint string
          )
          
          graph.addEdge(edge)
  
  return GraphBuildResult(
    graph: graph,
    conflicts: conflicts,
    warnings: warnings
  )

# Simplified graph builder for testing
proc buildSimpleGraph*(
  rootDemands: seq[VariantDemand],
  dependencyMap: Table[string, seq[VariantDemand]]
): GraphBuildResult =
  ## Simplified graph builder using a static dependency map
  ## 
  ## This is useful for testing where we want to control
  ## the dependency relationships explicitly.
  ## 
  ## Args:
  ##   rootDemands: Initial package demands
  ##   dependencyMap: Map of package name to its dependencies
  
  let provider: PackageProvider = proc(packageName: string): Option[seq[VariantDemand]] =
    if dependencyMap.hasKey(packageName):
      return some(dependencyMap[packageName])
    else:
      return none(seq[VariantDemand])
  
  return buildDependencyGraph(rootDemands, provider)

# Validate graph structure
proc validateGraph*(graph: DependencyGraph): bool =
  ## Validate that the dependency graph is well-formed
  ## 
  ## Checks:
  ## - All edge endpoints exist as terms
  ## - No self-loops
  ## - Edge lookup tables are consistent
  
  # Check all edges have valid endpoints
  for edge in graph.edges:
    if not graph.terms.hasKey(edge.fromTerm):
      return false
    if not graph.terms.hasKey(edge.toTerm):
      return false
    
    # Check for self-loops
    if edge.fromTerm == edge.toTerm:
      return false
  
  # Check edge lookup table consistency
  for termId in graph.terms.keys:
    let outgoing = graph.getOutgoingEdges(termId)
    let incoming = graph.getIncomingEdges(termId)
    
    # Verify outgoing edges are in main edge list
    for edge in outgoing:
      if edge notin graph.edges:
        return false
    
    # Verify incoming edges are in main edge list
    for edge in incoming:
      if edge notin graph.edges:
        return false
  
  return true

# Get root terms (terms with no incoming edges)
proc getRootTerms*(graph: DependencyGraph): seq[PackageTermId] =
  ## Get all root terms (terms with no incoming dependencies)
  ## 
  ## These are typically the packages directly requested by the user.
  
  result = @[]
  for termId in graph.terms.keys:
    if graph.getIncomingEdges(termId).len == 0:
      result.add(termId)

# Get leaf terms (terms with no outgoing edges)
proc getLeafTerms*(graph: DependencyGraph): seq[PackageTermId] =
  ## Get all leaf terms (terms with no outgoing dependencies)
  ## 
  ## These are typically low-level libraries with no dependencies.
  
  result = @[]
  for termId in graph.terms.keys:
    if graph.getOutgoingEdges(termId).len == 0:
      result.add(termId)

# Get terms by package name
proc getTermsByPackage*(graph: DependencyGraph, packageName: string): seq[PackageTerm] =
  ## Get all terms for a specific package name
  ## 
  ## This can return multiple terms if the same package appears
  ## with different variant profiles.
  
  result = @[]
  for term in graph.terms.values:
    if term.packageName == packageName:
      result.add(term)

# String representation for debugging
proc `$`*(buildResult: GraphBuildResult): string =
  ## String representation of graph build result
  
  result = "GraphBuildResult("
  result.add("terms=" & $buildResult.graph.getStats().terms)
  result.add(", edges=" & $buildResult.graph.getStats().edges)
  result.add(", conflicts=" & $buildResult.conflicts.len)
  result.add(", warnings=" & $buildResult.warnings.len)
  result.add(")")