## Dependency Graph - Core data structure for package dependencies
##
## This module implements the dependency graph used by the resolver to track
## package dependencies, detect cycles, and calculate installation order.

import tables
import sets
import sequtils
import strutils
import options
import ./variant_types
import ../manifest_parser

# ============================================================================
# Type Definitions
# ============================================================================

type
  PackageTermId* = string
    ## Unique identifier for a package term

  DependencyType* = enum
    Required,
    Optional

  PackageTerm* = object
    ## A specific package + version + variant combination
    id*: PackageTermId
    packageName*: string
    version*: SemanticVersion
    variantHash*: string  # xxh4-128 hash of variant profile
    variantProfile*: VariantProfile
    optional*: bool
    source*: string

  DependencyEdge* = object
    ## An edge from one package to its dependency
    fromTerm*: PackageTermId
    toTerm*: PackageTermId
    dependencyType*: DependencyType
    constraint*: string  # Version constraint (e.g. ">=1.0.0")

  DependencyGraph* = object
    ## The complete dependency graph
    terms*: Table[PackageTermId, PackageTerm]
    edges*: seq[DependencyEdge]
    incomingEdges*: Table[PackageTermId, seq[DependencyEdge]]
    outgoingEdges*: Table[PackageTermId, seq[DependencyEdge]]

  GraphStats* = object
    ## Statistics about the dependency graph
    terms*: int
    edges*: int
    roots*: int
    leaves*: int
    maxDepth*: int
    hasCycle*: bool

# ============================================================================
# Helper Functions
# ============================================================================

proc createTermId*(packageName, variantHash: string): PackageTermId =
  ## Create a term ID from components
  result = packageName & ":" & variantHash

proc termKey*(term: PackageTerm): PackageTermId =
  ## Generate unique key for a term
  result = term.id

proc `==`*(a, b: PackageTerm): bool =
  ## Compare two terms
  result = a.id == b.id

# ============================================================================
# Graph Operations
# ============================================================================

proc newDependencyGraph*(): DependencyGraph =
  ## Create an empty dependency graph
  result = DependencyGraph(
    terms: initTable[PackageTermId, PackageTerm](),
    edges: @[],
    incomingEdges: initTable[PackageTermId, seq[DependencyEdge]](),
    outgoingEdges: initTable[PackageTermId, seq[DependencyEdge]]()
  )

proc addTerm*(graph: var DependencyGraph, term: PackageTerm) =
  ## Add a term to the graph
  if term.id notin graph.terms:
    graph.terms[term.id] = term
    graph.incomingEdges[term.id] = @[]
    graph.outgoingEdges[term.id] = @[]

proc addEdge*(graph: var DependencyGraph, edge: DependencyEdge) =
  ## Add an edge to the graph
  # Ensure both nodes exist (should be added before edge)
  if edge.fromTerm notin graph.terms or edge.toTerm notin graph.terms:
    # In a robust system we might raise error or auto-add
    return 
  
  # Add to edge lists
  graph.edges.add(edge)
  graph.outgoingEdges[edge.fromTerm].add(edge)
  graph.incomingEdges[edge.toTerm].add(edge)

proc getTerm*(graph: DependencyGraph, termId: PackageTermId): Option[PackageTerm] =
  ## Get a term by ID
  if termId in graph.terms:
    return some(graph.terms[termId])
  else:
    return none(PackageTerm)

proc getIncomingEdges*(graph: DependencyGraph, termId: PackageTermId): seq[DependencyEdge] =
  ## Get all edges pointing to this node
  if termId in graph.incomingEdges:
    result = graph.incomingEdges[termId]
  else:
    result = @[]

proc getOutgoingEdges*(graph: DependencyGraph, termId: PackageTermId): seq[DependencyEdge] =
  ## Get all edges from this node
  if termId in graph.outgoingEdges:
    result = graph.outgoingEdges[termId]
  else:
    result = @[]

proc getDependencies*(graph: DependencyGraph, termId: PackageTermId): seq[PackageTerm] =
  ## Get all direct dependencies of a package
  let edges = graph.getOutgoingEdges(termId)
  result = edges.mapIt(graph.terms[it.toTerm])

proc getDependents*(graph: DependencyGraph, termId: PackageTermId): seq[PackageTerm] =
  ## Get all packages that depend on this one
  let edges = graph.getIncomingEdges(termId)
  result = edges.mapIt(graph.terms[it.fromTerm])

# ============================================================================
# Cycle Detection
# ============================================================================

proc hasCycle*(graph: DependencyGraph): bool =
  ## Check if the graph has any cycles using DFS
  var visited = initHashSet[PackageTermId]()
  var recursionStack = initHashSet[PackageTermId]()
  
  proc dfs(key: PackageTermId): bool =
    visited.incl(key)
    recursionStack.incl(key)
    
    if key in graph.outgoingEdges:
      for edge in graph.outgoingEdges[key]:
        let targetKey = edge.toTerm
        if targetKey notin visited:
          if dfs(targetKey):
            return true
        elif targetKey in recursionStack:
          return true
    
    recursionStack.excl(key)
    return false
  
  for key in graph.terms.keys:
    if key notin visited:
      if dfs(key):
        return true
  
  return false

proc findCycle*(graph: DependencyGraph): seq[PackageTerm] =
  ## Find a cycle in the graph (if one exists)
  var visited = initHashSet[PackageTermId]()
  var recursionStack = initHashSet[PackageTermId]()
  var path: seq[PackageTerm] = @[]
  
  proc dfs(key: PackageTermId): seq[PackageTerm] =
    visited.incl(key)
    recursionStack.incl(key)
    path.add(graph.terms[key])
    
    if key in graph.outgoingEdges:
      for edge in graph.outgoingEdges[key]:
        let targetKey = edge.toTerm
        if targetKey notin visited:
          let cycle = dfs(targetKey)
          if cycle.len > 0:
            return cycle
        elif targetKey in recursionStack:
          # Found cycle - extract it from path
          var cycleStart = 0
          for i in 0..<path.len:
            if path[i].id == targetKey:
              cycleStart = i
              break
          return path[cycleStart..^1] & @[graph.terms[targetKey]]
    
    path.setLen(path.len - 1)
    recursionStack.excl(key)
    return @[]
  
  for key in graph.terms.keys:
    if key notin visited:
      let cycle = dfs(key)
      if cycle.len > 0:
        return cycle
  
  return @[]

# ============================================================================
# Graph Analysis
# ============================================================================

proc topologicalSort*(graph: DependencyGraph): seq[PackageTermId] =
  ## Perform topological sort on the graph
  ## Returns a sequence of term IDs in topological order
  ## Raises ValueError if cycle is detected
  
  var visited = initHashSet[PackageTermId]()
  var recursionStack = initHashSet[PackageTermId]()
  var resultSeq: seq[PackageTermId] = @[]
  
  proc dfs(termId: PackageTermId) =
    visited.incl(termId)
    recursionStack.incl(termId)
    
    if termId in graph.outgoingEdges:
      for edge in graph.outgoingEdges[termId]:
        let targetId = edge.toTerm
        if targetId notin visited:
          dfs(targetId)
        elif targetId in recursionStack:
          raise newException(ValueError, "Cycle detected during topological sort")
    
    recursionStack.excl(termId)
    resultSeq.add(termId)
  
  for termId in graph.terms.keys:
    if termId notin visited:
      dfs(termId)
  
  # Reverse to get correct order (dependencies first? No, topological sort usually gives dependencies last if using this DFS)
  # Wait, standard DFS post-order gives reverse topological sort.
  # So if A -> B, B finishes first, then A. Result: B, A.
  # If we want installation order (dependencies first), we want B, A.
  # So this resultSeq is already in installation order (reverse topological sort).
  # Wait, topological sort of A -> B is A, B.
  # Installation order for A -> B (A depends on B) is B, A.
  # So we want B, A.
  # DFS post-order: B is visited, finishes. Added to result. A is visited, calls B (visited), finishes. Added to result.
  # Result: B, A.
  # So this IS the installation order.
  
  return resultSeq

proc nodeCount*(graph: DependencyGraph): int =
  ## Get the number of nodes/terms in the graph
  result = graph.terms.len

proc edgeCount*(graph: DependencyGraph): int =
  ## Get the number of edges in the graph
  result = graph.edges.len

proc getRoots*(graph: DependencyGraph): seq[PackageTerm] =
  ## Get all root nodes (nodes with no incoming edges)
  result = @[]
  for term in graph.terms.values:
    if graph.getIncomingEdges(term.id).len == 0:
      result.add(term)

proc getLeaves*(graph: DependencyGraph): seq[PackageTerm] =
  ## Get all leaf nodes (nodes with no outgoing edges)
  result = @[]
  for term in graph.terms.values:
    if graph.getOutgoingEdges(term.id).len == 0:
      result.add(term)

proc getDepth*(graph: DependencyGraph, termId: PackageTermId): int =
  ## Calculate the depth of a node (longest path from root)
  var visited = initHashSet[PackageTermId]()
  
  proc dfs(currentId: PackageTermId): int =
    if currentId in visited:
      return 0
    visited.incl(currentId)
    
    let edges = graph.getOutgoingEdges(currentId)
    if edges.len == 0:
      return 0
    
    var maxDepth = 0
    for edge in edges:
      let depth = dfs(edge.toTerm)
      if depth + 1 > maxDepth:
        maxDepth = depth + 1
    
    return maxDepth
  
  return dfs(termId)

proc getStats*(graph: DependencyGraph): GraphStats =
  ## Get statistics about the graph
  result = GraphStats(
    terms: graph.terms.len,
    edges: graph.edges.len,
    roots: graph.getRoots().len,
    leaves: graph.getLeaves().len,
    maxDepth: 0, # TODO: Calculate max depth efficiently
    hasCycle: graph.hasCycle()
  )

# ============================================================================
# String Representation
# ============================================================================

proc `$`*(term: PackageTerm): string =
  ## Convert term to string
  result = term.packageName & "@" & $term.version & "#" & term.variantHash[0..min(7, term.variantHash.high)]

proc `$`*(graph: DependencyGraph): string =
  ## Convert graph to string representation
  let stats = graph.getStats()
  result = "DependencyGraph(\n"
  result.add("  terms: " & $stats.terms & "\n")
  result.add("  edges: " & $stats.edges & "\n")
  result.add("  roots: " & $stats.roots & "\n")
  result.add("  leaves: " & $stats.leaves & "\n")
  result.add("  hasCycle: " & $stats.hasCycle & "\n")
  result.add(")")
