## CNF Translation for Dependency Resolution
##
## This module translates dependency constraints into Conjunctive Normal Form (CNF)
## for use with CDCL-based SAT solving.
##
## Philosophy:
## - Each package+version+variant combination is a boolean variable
## - Dependencies become implication clauses (A → B ≡ ¬A ∨ B)
## - Exclusivity becomes mutual exclusion clauses (¬(A ∧ B) ≡ ¬A ∨ ¬B)
## - Variant satisfaction becomes satisfaction clauses
##
## Key Concepts:
## - A CNF formula is a conjunction of disjunctions (AND of ORs)
## - Each clause is a disjunction of literals (OR of variables/negations)
## - The solver finds an assignment that satisfies all clauses

import std/[tables, sets, hashes, options]
import ./solver_types
import ./variant_types
import ../manifest_parser

type
  ## A boolean variable representing a specific package+version+variant
  ## This is the atomic unit of the CNF formula
  BoolVar* = object
    package*: PackageId
    version*: SemanticVersion
    variant*: VariantProfile
  
  ## A literal is a boolean variable or its negation
  Literal* = object
    variable*: BoolVar
    isNegated*: bool
  
  ## A clause is a disjunction of literals (OR)
  ## Example: (¬A ∨ B ∨ ¬C) means "NOT A OR B OR NOT C"
  Clause* = object
    literals*: seq[Literal]
    reason*: string  # Human-readable explanation
  
  ## A CNF formula is a conjunction of clauses (AND)
  ## Example: (A ∨ B) ∧ (¬A ∨ C) means "(A OR B) AND (NOT A OR C)"
  CNFFormula* = object
    clauses*: seq[Clause]
    variables*: Table[BoolVar, int]  # Variable → unique ID
    nextVarId*: int
  
  ## The type of clause (for debugging and error reporting)
  ClauseKind* = enum
    DependencyClause,      ## A → B (dependency implication)
    ExclusivityClause,     ## ¬(A ∧ B) (mutual exclusion)
    SatisfactionClause,    ## Variant requirements
    RootClause             ## User requirements

# --- BoolVar Operations ---

proc `==`*(a, b: BoolVar): bool =
  ## Equality for boolean variables
  result = a.package == b.package and
           a.version == b.version and
           a.variant.hash == b.variant.hash

proc hash*(v: BoolVar): Hash =
  ## Hash function for boolean variables
  var h: Hash = 0
  h = h !& hash(v.package)
  h = h !& hash($v.version)
  h = h !& hash(v.variant.hash)
  result = !$h

proc `$`*(v: BoolVar): string =
  ## String representation of a boolean variable
  result = v.package & "=" & $v.version
  if v.variant.domains.len > 0:
    result.add(" [" & v.variant.hash & "]")

# --- Literal Operations ---

proc makeLiteral*(variable: BoolVar, isNegated: bool = false): Literal =
  ## Create a literal from a boolean variable
  result = Literal(variable: variable, isNegated: isNegated)

proc negate*(lit: Literal): Literal =
  ## Negate a literal
  result = Literal(variable: lit.variable, isNegated: not lit.isNegated)

proc `$`*(lit: Literal): string =
  ## String representation of a literal
  if lit.isNegated:
    result = "¬" & $lit.variable
  else:
    result = $lit.variable

# --- Clause Operations ---

proc makeClause*(literals: seq[Literal], reason: string = ""): Clause =
  ## Create a clause from literals
  result = Clause(literals: literals, reason: reason)

proc `$`*(clause: Clause): string =
  ## String representation of a clause
  result = "("
  for i, lit in clause.literals:
    if i > 0:
      result.add(" ∨ ")
    result.add($lit)
  result.add(")")
  if clause.reason.len > 0:
    result.add(" [" & clause.reason & "]")

# --- CNF Formula Operations ---

proc newCNFFormula*(): CNFFormula =
  ## Create a new empty CNF formula
  result = CNFFormula(
    clauses: @[],
    variables: initTable[BoolVar, int](),
    nextVarId: 1
  )

proc getOrCreateVarId*(formula: var CNFFormula, variable: BoolVar): int =
  ## Get or create a unique ID for a boolean variable
  if formula.variables.hasKey(variable):
    return formula.variables[variable]
  else:
    let id = formula.nextVarId
    formula.variables[variable] = id
    formula.nextVarId += 1
    return id

proc addClause*(formula: var CNFFormula, clause: Clause) =
  ## Add a clause to the CNF formula
  formula.clauses.add(clause)

proc `$`*(formula: CNFFormula): string =
  ## String representation of a CNF formula
  result = "CNF Formula (" & $formula.clauses.len & " clauses, " & 
           $formula.variables.len & " variables):\n"
  for i, clause in formula.clauses:
    result.add("  " & $i & ": " & $clause & "\n")

# --- CNF Translation Functions ---

proc termToBoolVar*(term: Term, version: SemanticVersion): BoolVar =
  ## Convert a term to a boolean variable
  ## This creates a specific package+version+variant combination
  result = BoolVar(
    package: term.package,
    version: version,
    variant: term.constraint.variantReq
  )

proc translateDependency*(
  formula: var CNFFormula,
  dependent: PackageId,
  dependentVersion: SemanticVersion,
  dependentVariant: VariantProfile,
  dependency: PackageId,
  dependencyVersion: SemanticVersion,
  dependencyVariant: VariantProfile
): Clause =
  ## Translate a dependency into a CNF clause
  ## "A depends on B" becomes "¬A ∨ B" (if A then B)
  ##
  ## Requirements: 6.2 - WHEN encoding dependencies THEN the system SHALL create implication clauses (A → B)
  
  let varA = BoolVar(
    package: dependent,
    version: dependentVersion,
    variant: dependentVariant
  )
  
  let varB = BoolVar(
    package: dependency,
    version: dependencyVersion,
    variant: dependencyVariant
  )
  
  # Register variables
  discard formula.getOrCreateVarId(varA)
  discard formula.getOrCreateVarId(varB)
  
  # Create clause: ¬A ∨ B
  let clause = makeClause(
    @[
      makeLiteral(varA, isNegated = true),  # ¬A
      makeLiteral(varB, isNegated = false)  # B
    ],
    reason = dependent & " " & $dependentVersion & " depends on " & 
             dependency & " " & $dependencyVersion
  )
  
  formula.addClause(clause)
  return clause

proc translateExclusivity*(
  formula: var CNFFormula,
  packageA: PackageId,
  versionA: SemanticVersion,
  variantA: VariantProfile,
  packageB: PackageId,
  versionB: SemanticVersion,
  variantB: VariantProfile,
  reason: string = ""
): Clause =
  ## Translate mutual exclusion into a CNF clause
  ## "A and B are mutually exclusive" becomes "¬A ∨ ¬B" (not both)
  ##
  ## Requirements: 6.3 - WHEN encoding exclusivity THEN the system SHALL create mutual exclusion clauses (¬(A ∧ B))
  
  let varA = BoolVar(
    package: packageA,
    version: versionA,
    variant: variantA
  )
  
  let varB = BoolVar(
    package: packageB,
    version: versionB,
    variant: variantB
  )
  
  # Register variables
  discard formula.getOrCreateVarId(varA)
  discard formula.getOrCreateVarId(varB)
  
  # Create clause: ¬A ∨ ¬B
  let clause = makeClause(
    @[
      makeLiteral(varA, isNegated = true),  # ¬A
      makeLiteral(varB, isNegated = true)   # ¬B
    ],
    reason = if reason.len > 0: reason else: "Mutually exclusive: " & 
             packageA & " and " & packageB
  )
  
  formula.addClause(clause)
  return clause

proc translateVariantSatisfaction*(
  formula: var CNFFormula,
  package: PackageId,
  version: SemanticVersion,
  requiredVariant: VariantProfile,
  availableVariant: VariantProfile
): Clause =
  ## Translate variant satisfaction into a CNF clause
  ## "If we select this package, its variant must satisfy requirements"
  ##
  ## Requirements: 6.4 - WHEN encoding variant satisfaction THEN the system SHALL create satisfaction clauses
  
  let varRequired = BoolVar(
    package: package,
    version: version,
    variant: requiredVariant
  )
  
  let varAvailable = BoolVar(
    package: package,
    version: version,
    variant: availableVariant
  )
  
  # Register variables
  discard formula.getOrCreateVarId(varRequired)
  discard formula.getOrCreateVarId(varAvailable)
  
  # Check if available variant satisfies required variant
  # For now, we check if all required domains/flags are present
  var satisfies = true
  for domain, variantDomain in requiredVariant.domains.pairs:
    if not availableVariant.domains.hasKey(domain):
      satisfies = false
      break
    
    for flag in variantDomain.flags:
      if flag notin availableVariant.domains[domain].flags:
        satisfies = false
        break
  
  if satisfies:
    # If available satisfies required, create: ¬required ∨ available
    # Meaning: if we need required, we can use available
    let clause = makeClause(
      @[
        makeLiteral(varRequired, isNegated = true),
        makeLiteral(varAvailable, isNegated = false)
      ],
      reason = "Variant " & availableVariant.hash & " satisfies " & requiredVariant.hash
    )
    formula.addClause(clause)
    return clause
  else:
    # If available doesn't satisfy required, create: ¬required ∨ ¬available
    # Meaning: we can't have both (they're incompatible)
    let clause = makeClause(
      @[
        makeLiteral(varRequired, isNegated = true),
        makeLiteral(varAvailable, isNegated = true)
      ],
      reason = "Variant " & availableVariant.hash & " does not satisfy " & requiredVariant.hash
    )
    formula.addClause(clause)
    return clause

proc translateRootRequirement*(
  formula: var CNFFormula,
  package: PackageId,
  version: SemanticVersion,
  variant: VariantProfile
): Clause =
  ## Translate a root requirement into a CNF clause
  ## "User requires package P" becomes "P" (unit clause)
  ##
  ## Requirements: 6.1 - WHEN translating to CNF THEN the system SHALL create boolean variables for each term
  
  let variable = BoolVar(
    package: package,
    version: version,
    variant: variant
  )
  
  # Register variable
  discard formula.getOrCreateVarId(variable)
  
  # Create unit clause: P
  let clause = makeClause(
    @[makeLiteral(variable, isNegated = false)],
    reason = "User requires " & package & " " & $version
  )
  
  formula.addClause(clause)
  return clause

proc translateIncompatibility*(formula: var CNFFormula, incomp: Incompatibility): Clause =
  ## Translate an incompatibility into a CNF clause
  ## An incompatibility ¬(T1 ∧ T2 ∧ ... ∧ Tn) becomes (¬T1 ∨ ¬T2 ∨ ... ∨ ¬Tn)
  ##
  ## This is the general translation that handles all incompatibility types
  
  var literals: seq[Literal] = @[]
  
  for term in incomp.terms:
    # For each term in the incompatibility, we need to create a literal
    # The term already has a constraint with version and variant
    # We need to pick a specific version that satisfies the constraint
    
    # We create a boolean variable representing the term's constraint
    # In a full implementation, this would map to specific package versions
    # For now, we use the term's constraint as the identity of the variable
    
    # We need to ensure we have a valid version for the BoolVar
    # Since Incompatibility terms might be ranges, we might need a different approach
    # or map to a specific representative version.
    
    # For this MVP, we'll assume the term maps to a specific "decision" variable
    # that the solver is tracking.
    
    let variable = BoolVar(
      package: term.package,
      version: parseSemanticVersion("0.0.0"), # Placeholder/Any
      variant: term.constraint.variantReq
    )
    
    discard formula.getOrCreateVarId(variable)
    
    # If term is positive (P satisfies C), then in the incompatibility ¬(P satisfies C),
    # we want ¬(Variable). So we add ¬Variable to the clause.
    # If term is negative (P satisfies NOT C), then in the incompatibility ¬(P satisfies NOT C),
    # we want ¬(¬Variable) = Variable. So we add Variable to the clause.
    
    let isNegated = term.isPositive
    literals.add(makeLiteral(variable, isNegated = isNegated))
  
  let clause = makeClause(literals, reason = incomp.externalContext)
  formula.addClause(clause)
  return clause

import ./dependency_graph

proc translateGraph*(formula: var CNFFormula, graph: DependencyGraph) =
  ## Translate a dependency graph into CNF clauses
  ## This converts the graph structure (nodes and edges) into boolean logic
  
  for termId, term in graph.terms.pairs:
    # 1. Create variable for each term
    let version = term.version
    
    let variable = BoolVar(
      package: term.packageName,
      version: version,
      variant: term.variantProfile
    )
    discard formula.getOrCreateVarId(variable)
    
    # 2. Translate dependencies (Edges)
    # A -> B becomes ¬A ∨ B
    for edge in graph.getOutgoingEdges(termId):
      let depTerm = graph.terms[edge.toTerm]
      let depVersion = depTerm.version
      
      discard translateDependency(
        formula,
        term.packageName, version, term.variantProfile,
        depTerm.packageName, depVersion, depTerm.variantProfile
      )

# --- Validation ---

proc isValidCNF*(formula: CNFFormula): bool =
  ## Validate that the CNF formula is well-formed
  ##
  ## Requirements: 6.5 - WHEN CNF is complete THEN the system SHALL be ready for CDCL solving
  
  # Check that we have at least one clause
  if formula.clauses.len == 0:
    return false
  
  # Check that each clause has at least one literal
  for clause in formula.clauses:
    if clause.literals.len == 0:
      return false
  
  # Check that all variables in clauses are registered
  for clause in formula.clauses:
    for literal in clause.literals:
      if not formula.variables.hasKey(literal.variable):
        return false
  
  return true
