## Conflict Detection for Dependency Resolution
##
## This module implements specific conflict detection for the NIP dependency resolver.
## It detects and reports various types of conflicts that can occur during resolution:
## - Version conflicts: Incompatible version requirements
## - Variant conflicts: Incompatible variant flags
## - Circular dependencies: Cycles in the dependency graph
## - Missing packages: Packages not found in any source
##
## Philosophy:
## - Detect conflicts early and specifically
## - Provide actionable error messages
## - Suggest solutions when possible
## - Track conflict origins for debugging
##
## Requirements:
## - 7.1: Report version conflicts with incompatible version requirements
## - 7.2: Report variant conflicts with incompatible variant flags
## - 7.3: Report circular dependencies with cycle path
## - 7.4: Report missing packages with suggestions
## - 7.5: Provide actionable suggestions for resolution

import std/[tables, sets, options, sequtils, algorithm, strutils, strformat]
import ./solver_types
import ./variant_types
import ../manifest_parser

type
  ## The kind of conflict detected
  ConflictKind* = enum
    VersionConflict,      ## Incompatible version requirements
    VariantConflict,      ## Incompatible variant flags
    CircularDependency,   ## Cycle in dependency graph
    MissingPackage,       ## Package not found in any source
    BuildHashMismatch     ## Installed build doesn't match required

  ## Detailed information about a conflict
  ConflictReport* = object
    kind*: ConflictKind
    packages*: seq[string]           ## Packages involved in the conflict
    details*: string                 ## Detailed description of the conflict
    suggestions*: seq[string]        ## Actionable suggestions for resolution
    conflictingTerms*: seq[Term]     ## The specific terms that conflict
    cyclePath*: Option[seq[string]]  ## For circular dependencies: the cycle path

# --- Version Conflict Detection ---

proc detectVersionConflict*(
  package: PackageId,
  constraints: seq[VersionConstraint]
): Option[ConflictReport] =
  ## Detect if a set of version constraints are incompatible
  ##
  ## Requirements: 7.1 - Report version conflicts with incompatible version requirements
  ##
  ## Returns a ConflictReport if the constraints are incompatible, None otherwise
  
  if constraints.len < 2:
    return none(ConflictReport)
  
  # Check if all constraints can be satisfied simultaneously
  # For now, we use a simple approach: check if any two constraints are incompatible
  
  for i in 0 ..< constraints.len:
    for j in (i + 1) ..< constraints.len:
      let constraint1 = constraints[i]
      let constraint2 = constraints[j]
      
      # Check if these constraints can both be satisfied
      # This is a simplified check - a full implementation would need
      # proper semantic version range intersection logic
      
      case constraint1.operator:
      of OpExact:
        # Exact version must match
        case constraint2.operator:
        of OpExact:
          if constraint1.version != constraint2.version:
            return some(ConflictReport(
              kind: VersionConflict,
              packages: @[package],
              details: fmt"Package '{package}' has conflicting exact version requirements: {constraint1.version} and {constraint2.version}",
              suggestions: @[
                fmt"Check which packages require {package} {constraint1.version}",
                fmt"Check which packages require {package} {constraint2.version}",
                "Consider using a version that satisfies both requirements",
                "Or use NipCell isolation to install different versions in separate environments"
              ],
              conflictingTerms: @[],
              cyclePath: none(seq[string])
            ))
        of OpGreaterEq:
          if constraint1.version < constraint2.version:
            return some(ConflictReport(
              kind: VersionConflict,
              packages: @[package],
              details: fmt"Package '{package}' requires exact version {constraint1.version} but also requires >= {constraint2.version}",
              suggestions: @[
                fmt"Update requirement to {constraint2.version} or later",
                "Check if {constraint1.version} is still maintained",
                "Consider upgrading to a newer version"
              ],
              conflictingTerms: @[],
              cyclePath: none(seq[string])
            ))
        else:
          discard  # Other operators would need more complex logic
      else:
        discard  # Other operators would need more complex logic
  
  return none(ConflictReport)

# --- Variant Conflict Detection ---

proc detectVariantConflict*(
  package: PackageId,
  demands: seq[VariantDemand]
): Option[ConflictReport] =
  ## Detect if a set of variant demands are incompatible
  ##
  ## Requirements: 7.2 - Report variant conflicts with incompatible variant flags
  ##
  ## Returns a ConflictReport if the demands are incompatible, None otherwise
  
  if demands.len < 2:
    return none(ConflictReport)
  
  # Check for exclusive domain conflicts
  var domainValues: Table[string, seq[string]] = initTable[string, seq[string]]()
  
  for demand in demands:
    for domain, variantDomain in demand.variantProfile.domains.pairs:
      if domain notin domainValues:
        domainValues[domain] = @[]
      
      for flag in variantDomain.flags:
        if flag notin domainValues[domain]:
          domainValues[domain].add(flag)
  
  # Check for conflicts in exclusive domains
  for domain, values in domainValues.pairs:
    # Check if this is an exclusive domain (by checking first demand)
    var isExclusive = false
    for demand in demands:
      if domain in demand.variantProfile.domains:
        isExclusive = demand.variantProfile.domains[domain].exclusivity == Exclusive
        break
    
    if isExclusive and values.len > 1:
      # Exclusive domain has multiple values - conflict!
      let conflictingDemands = demands.filterIt(domain in it.variantProfile.domains)
      let valuesList = values.join(", ")
      
      return some(ConflictReport(
        kind: VariantConflict,
        packages: @[package],
        details: fmt"Package '{package}' has conflicting exclusive variant flags in domain '{domain}': {valuesList}",
        suggestions: @[
          fmt"Choose one of the conflicting values: {valuesList}",
          "Check which packages require each variant",
          "Consider using NipCell isolation to install different variants in separate environments",
          "Or rebuild the package with a compatible variant"
        ],
        conflictingTerms: @[],
        cyclePath: none(seq[string])
      ))
  
  return none(ConflictReport)

# --- Circular Dependency Detection ---

proc detectCircularDependency*(
  graph: Table[PackageId, seq[PackageId]],
  startPackage: PackageId
): Option[ConflictReport] =
  ## Detect if there is a circular dependency starting from a package
  ##
  ## Requirements: 7.3 - Report circular dependencies with cycle path
  ##
  ## Returns a ConflictReport with the cycle path if a cycle is found, None otherwise
  
  var visited: HashSet[PackageId] = initHashSet[PackageId]()
  var recursionStack: HashSet[PackageId] = initHashSet[PackageId]()
  var path: seq[PackageId] = @[]
  
  proc dfs(package: PackageId): Option[seq[PackageId]] =
    visited.incl(package)
    recursionStack.incl(package)
    path.add(package)
    
    if package in graph:
      for dependency in graph[package]:
        if dependency notin visited:
          let cyclePath = dfs(dependency)
          if cyclePath.isSome:
            return cyclePath
        elif dependency in recursionStack:
          # Found a cycle!
          let cycleStart = path.find(dependency)
          if cycleStart >= 0:
            let cycle = path[cycleStart..^1] & @[dependency]
            return some(cycle)
    
    discard path.pop()
    recursionStack.excl(package)
    return none(seq[PackageId])
  
  let cyclePath = dfs(startPackage)
  
  if cyclePath.isSome:
    let cycle = cyclePath.get()
    let cycleStr = cycle.join(" -> ")
    return some(ConflictReport(
      kind: CircularDependency,
      packages: cycle,
      details: fmt"Circular dependency detected: {cycleStr}",
      suggestions: @[
        "Break the cycle by removing or modifying one of the dependencies",
        "Check if any dependencies are optional and can be made optional",
        "Consider splitting the package into smaller packages",
        "Review the dependency declarations for correctness"
      ],
      conflictingTerms: @[],
      cyclePath: cyclePath
    ))
  
  return none(ConflictReport)

# --- Missing Package Detection ---

proc detectMissingPackage*(
  package: PackageId,
  availablePackages: HashSet[PackageId]
): Option[ConflictReport] =
  ## Detect if a required package is missing from all sources
  ##
  ## Requirements: 7.4 - Report missing packages with suggestions
  ##
  ## Returns a ConflictReport if the package is missing, None otherwise
  
  if package in availablePackages:
    return none(ConflictReport)
  
  # Find similar package names for suggestions
  var suggestions: seq[string] = @[]
  
  # Simple similarity check: packages with similar names
  let packageLower = package.toLowerAscii()
  var similarPackages: seq[string] = @[]
  
  for available in availablePackages:
    let availableLower = available.toLowerAscii()
    
    # Check for substring matches or similar names
    if availableLower.contains(packageLower) or packageLower.contains(availableLower):
      similarPackages.add(available)
    
    # Check for edit distance (simple check for typos)
    if abs(available.len - package.len) <= 2:
      var matches = 0
      for i in 0 ..< min(available.len, package.len):
        if available[i] == package[i]:
          matches += 1
      
      if matches >= min(available.len, package.len) - 2:
        similarPackages.add(available)
  
  # Build suggestions
  suggestions.add(fmt"Package '{package}' not found in any configured repository")
  
  if similarPackages.len > 0:
    let similarStr = similarPackages.join(", ")
    suggestions.add(fmt"Did you mean: {similarStr}?")
  
  suggestions.add("Check if the package name is spelled correctly")
  suggestions.add("Check if the package is available in your configured repositories")
  suggestions.add("Try updating your package repository metadata")
  suggestions.add("Check if the package has been renamed or moved")
  
  return some(ConflictReport(
    kind: MissingPackage,
    packages: @[package],
    details: fmt"Package '{package}' not found in any source",
    suggestions: suggestions,
    conflictingTerms: @[],
    cyclePath: none(seq[string])
  ))

# --- Build Hash Mismatch Detection ---

proc detectBuildHashMismatch*(
  package: PackageId,
  expectedHash: string,
  actualHash: string
): Option[ConflictReport] =
  ## Detect if an installed package's build hash doesn't match the expected hash
  ##
  ## Requirements: 7.5 - Provide actionable suggestions for resolution
  ##
  ## Returns a ConflictReport if hashes don't match, None otherwise
  
  if expectedHash == actualHash:
    return none(ConflictReport)
  
  return some(ConflictReport(
    kind: BuildHashMismatch,
    packages: @[package],
    details: fmt"Package '{package}' build hash mismatch: expected {expectedHash}, got {actualHash}",
    suggestions: @[
      "The installed package may have been modified or corrupted",
      "Try reinstalling the package",
      "Check if the package source has changed",
      "Verify the integrity of your package cache",
      "Consider running 'nip verify' to check all packages"
    ],
    conflictingTerms: @[],
    cyclePath: none(seq[string])
  ))

# --- Conflict Reporting ---

proc formatConflict*(report: ConflictReport): string =
  ## Format a conflict report as a human-readable error message
  ##
  ## Requirements: 7.1, 7.2, 7.3, 7.4, 7.5
  
  result = ""
  
  case report.kind:
  of VersionConflict:
    result = fmt"""
❌ [VersionConflict] Cannot satisfy conflicting version requirements
🔍 Context: {report.details}
💡 Suggestions:"""
    for suggestion in report.suggestions:
      result.add(fmt"\n  • {suggestion}")
  
  of VariantConflict:
    result = fmt"""
❌ [VariantConflict] Cannot unify conflicting variant demands
🔍 Context: {report.details}
💡 Suggestions:"""
    for suggestion in report.suggestions:
      result.add(fmt"\n  • {suggestion}")
  
  of CircularDependency:
    result = fmt"""
❌ [CircularDependency] Circular dependency detected
🔍 Context: {report.details}
💡 Suggestions:"""
    for suggestion in report.suggestions:
      result.add(fmt"\n  • {suggestion}")
  
  of MissingPackage:
    result = fmt"""
❌ [MissingPackage] Package not found
🔍 Context: {report.details}
💡 Suggestions:"""
    for suggestion in report.suggestions:
      result.add(fmt"\n  • {suggestion}")
  
  of BuildHashMismatch:
    result = fmt"""
❌ [BuildHashMismatch] Build hash verification failed
🔍 Context: {report.details}
💡 Suggestions:"""
    for suggestion in report.suggestions:
      result.add(fmt"\n  • {suggestion}")
  
  return result

# --- Conflict Extraction ---

proc extractMinimalConflict*(
  incompatibilities: seq[Incompatibility]
): Option[seq[Incompatibility]] =
  ## Extract the minimal set of incompatibilities that cause a conflict
  ##
  ## Requirements: 7.5 - Provide minimal conflicting requirements
  ##
  ## This removes redundant incompatibilities to show only the essential conflict.
  ## Uses a greedy algorithm to find a minimal unsatisfiable core (MUC).
  ##
  ## Algorithm:
  ## 1. Start with all incompatibilities
  ## 2. Try removing each incompatibility one at a time
  ## 3. If the remaining set is still unsatisfiable, keep it removed
  ## 4. Repeat until no more incompatibilities can be removed
  ##
  ## This is a greedy approximation of the MUC problem (which is NP-hard).
  ## For practical purposes, this gives good results quickly.
  
  if incompatibilities.len == 0:
    return none(seq[Incompatibility])
  
  if incompatibilities.len == 1:
    return some(incompatibilities)
  
  # Start with all incompatibilities
  var minimal = incompatibilities
  var changed = true
  
  # Iteratively try to remove incompatibilities
  while changed:
    changed = false
    var i = 0
    
    while i < minimal.len:
      # Try removing incompatibility at index i
      let candidate = minimal[0 ..< i] & minimal[(i + 1) ..< minimal.len]
      
      # Check if the candidate set is still unsatisfiable
      # For now, we use a simple heuristic: if there are still conflicting terms,
      # the set is likely still unsatisfiable
      
      # Collect all packages mentioned in the candidate set
      var packages: HashSet[string] = initHashSet[string]()
      for incomp in candidate:
        for term in incomp.terms:
          packages.incl(term.package)
      
      # If we still have packages with conflicting requirements, keep the candidate
      # This is a simplified check - a full implementation would need to re-solve
      if packages.len > 0:
        minimal = candidate
        changed = true
        break
      
      i += 1
  
  return some(minimal)

# --- Conflict Analysis ---

proc analyzeConflictOrigins*(
  report: ConflictReport,
  packageManifests: Table[PackageId, seq[VariantDemand]]
): seq[string] =
  ## Analyze the origins of a conflict and provide detailed context
  ##
  ## Requirements: 7.5 - Provide actionable suggestions
  
  var analysis: seq[string] = @[]
  
  case report.kind:
  of VersionConflict:
    for package in report.packages:
      if package in packageManifests:
        analysis.add(fmt"Package '{package}' has {packageManifests[package].len} version demands")
  
  of VariantConflict:
    for package in report.packages:
      if package in packageManifests:
        analysis.add(fmt"Package '{package}' has {packageManifests[package].len} variant demands")
  
  of CircularDependency:
    if report.cyclePath.isSome:
      let cycle = report.cyclePath.get()
      let cycleStr = cycle.join(" -> ")
      analysis.add(fmt"Cycle involves {cycle.len} packages: {cycleStr}")
  
  of MissingPackage:
    analysis.add(fmt"Package '{report.packages[0]}' is required but not available")
  
  of BuildHashMismatch:
    analysis.add(fmt"Package '{report.packages[0]}' integrity check failed")
  
  return analysis

