# nimpak/dependency.nim
# Dependency graph resolution and management system

import std/[tables, sets, sequtils, algorithm, strformat]
import ../nip/types

type
  DependencyGraph* = object
    nodes*: Table[PackageId, Fragment]
    edges*: Table[PackageId, seq[PackageId]]
    resolved*: seq[PackageId]

  InstallOrder* = object
    packages*: seq[PackageId]
    totalSteps*: int

  DependencyError* = object of NimPakError
    conflictingPackages*: seq[PackageId]
    missingDependencies*: seq[PackageId]
    cyclicDependencies*: seq[PackageId]

# Public API
proc resolveDependencies*(root: PackageId, fragments: Table[PackageId, Fragment]): Result[InstallOrder, DependencyError] =
  ## Resolve dependencies for a root package and return installation order
  var graph = DependencyGraph()

  # Build the dependency graph
  let buildResult = buildDependencyGraph(graph, root, fragments)
  if buildResult.isErr:
    return err(buildResult.error)

  # Perform topological sort to get installation order
  let sortResult = topologicalSort(graph)
  if sortResult.isErr:
    return err(sortResult.error)

  ok(InstallOrder(
    packages: sortResult.get(),
    totalSteps: sortResult.get().len
  ))

proc buildDependencyGraph(graph: var DependencyGraph, root: PackageId, fragments: Table[PackageId, Fragment]): Result[void, DependencyError] =
  ## Build directed graph from package dependencies (6.1.1, 6.1.2)
  var visited = initHashSet[PackageId]()
  var visiting = initHashSet[PackageId]()

  proc visitNode(pkgId: PackageId): Result[void, DependencyError] =
    if pkgId in visiting:
      # Cycle detected
      return err(DependencyError(
        code: DependencyConflict,
        msg: "Circular dependency detected",
        cyclicDependencies: @[pkgId]
      ))

    if pkgId in visited:
      return ok()

    # Check if fragment exists
    if pkgId notin fragments:
      return err(DependencyError(
        code: PackageNotFound,
        msg: "Missing dependency: " & pkgId.name,
        missingDependencies: @[pkgId]
      ))

    visiting.incl(pkgId)
    let fragment = fragments[pkgId]

    # Add node to graph
    graph.nodes[pkgId] = fragment
    graph.edges[pkgId] = fragment.dependencies

    # Visit dependencies recursively
    for dep in fragment.dependencies:
      let depResult = visitNode(dep)
      if depResult.isErr:
        return depResult

    visiting.excl(pkgId)
    visited.incl(pkgId)
    ok()

  visitNode(root)

proc topologicalSort(graph: DependencyGraph): Result[seq[PackageId], DependencyError] =
  ## Perform topological sort to determine installation order (6.1.3)
  var inDegree = initTable[PackageId, int]()
  var queue: seq[PackageId] = @[]
  var result: seq[PackageId] = @[]

  # Initialize in-degree count
  for node in graph.nodes.keys:
    inDegree[node] = 0

  # Calculate in-degrees
  for (node, deps) in graph.edges.pairs:
    for dep in deps:
      if dep in inDegree:
        inDegree[dep] += 1

  # Find nodes with no incoming edges
  for (node, degree) in inDegree.pairs:
    if degree == 0:
      queue.add(node)

  # Process queue
  while queue.len > 0:
    let current = queue.pop()
    result.add(current)

    # Reduce in-degree for dependencies
    if current in graph.edges:
      for dep in graph.edges[current]:
        if dep in inDegree:
          inDegree[dep] -= 1
          if inDegree[dep] == 0:
            queue.add(dep)

  # Check for cycles
  if result.len != graph.nodes.len:
    let remaining = toSeq(graph.nodes.keys).filterIt(it notin result)
    return err(DependencyError(
      code: DependencyConflict,
      msg: "Circular dependencies detected",
      cyclicDependencies: remaining
    ))

  # Reverse to get correct installation order (dependencies first)
  result.reverse()
  ok(result)

proc resolveVersionConstraint*(pkg: string, constraint: string): Result[PackageId, DependencyError] =
  ## Stub for version constraint resolution (6.1.4)
  # TODO: Implement semantic version constraint resolution
  # For now, return a basic PackageId
  ok(PackageId(name: pkg, version: "latest", stream: Stable))

proc validateDependencies*(fragments: Table[PackageId, Fragment]): Result[void, DependencyError] =
  ## Validate all dependencies exist and are consistent
  var missingDeps: seq[PackageId] = @[]

  for (pkgId, fragment) in fragments.pairs:
    for dep in fragment.dependencies:
      if dep notin fragments:
        missingDeps.add(dep)

  if missingDeps.len > 0:
    return err(DependencyError(
      code: PackageNotFound,
      msg: "Missing dependencies found",
      missingDependencies: missingDeps
    ))

  ok()

# Helper functions for diagnostics (6.1.5)
proc formatDependencyError*(err: DependencyError): string =
  ## Format dependency error with useful diagnostics
  result = fmt"Dependency Error: {err.msg}\n"

  if err.missingDependencies.len > 0:
    result.add("Missing Dependencies:\n")
    for dep in err.missingDependencies:
      result.add(fmt"  - {dep.name} {dep.version}\n")

  if err.cyclicDependencies.len > 0:
    result.add("Circular Dependencies:\n")
    for dep in err.cyclicDependencies:
      result.add(fmt"  - {dep.name} {dep.version}\n")

  if err.conflictingPackages.len > 0:
    result.add("Conflicting Packages:\n")
    for dep in err.conflictingPackages:
      result.add(fmt"  - {dep.name} {dep.version}\n")

proc getDependencyTree*(root: PackageId, fragments: Table[PackageId, Fragment]): Result[string, DependencyError] =
  ## Generate a visual dependency tree for debugging
  var output = ""
  var visited = initHashSet[PackageId]()

  proc printTree(pkgId: PackageId, indent: int = 0) =
    let prefix = "  ".repeat(indent)
    output.add(fmt"{prefix}- {pkgId.name} {pkgId.version}\n")

    if pkgId in visited:
      output.add(fmt"{prefix}  (already processed)\n")
      return

    visited.incl(pkgId)

    if pkgId in fragments:
      let fragment = fragments[pkgId]
      for dep in fragment.dependencies:
        printTree(dep, indent + 1)

  printTree(root)
  ok(output)