## Solver Data Structures for PubGrub-Style Dependency Resolution
##
## This module defines the core data structures for the PubGrub solver,
## adapted for NexusOS variant system.
##
## Philosophy:
## - Terms represent assertions about packages (positive or negative)
## - Incompatibilities represent mutually exclusive states
## - Assignments track the solver's current decisions
## - Derivations provide human-readable error messages
##
## Key Concepts:
## - A Term is "Package P satisfies Constraint C"
## - An Incompatibility is "¬(Term1 ∧ Term2 ∧ ... ∧ TermN)"
## - The solver finds an Assignment that satisfies all Incompatibilities

import std/[strutils, hashes, tables, sets, options]
import ../manifest_parser  # For SemanticVersion, VersionConstraint
import ./variant_types     # For VariantProfile

type
  PackageId* = string
  
  ## A constraint on a package's version and variants
  ## This represents the mathematical "Range" of valid states
  Constraint* = object
    versionRange*: VersionConstraint
    variantReq*: VariantProfile
    
    # If true, this constraint implies "NOT this range"
    isNegative*: bool
  
  ## A Term is a specific assertion about a package
  ## Logic: "Package P satisfies Constraint C"
  ## Example: Term(nginx, >=1.20 +wayland)
  Term* = object
    package*: PackageId
    constraint*: Constraint
  
  ## The cause of an incompatibility (for error reporting)
  ## This enables PubGrub's human-readable error messages
  IncompatibilityCause* = enum
    Root,              ## The user requested this
    Dependency,        ## Package A depends on B
    VariantConflict,   ## +wayland vs +x11 are mutually exclusive
    BuildHashMismatch, ## Different build configurations conflict
    NoVersions,        ## No versions satisfy the constraint
    PackageNotFound    ## Package doesn't exist in any source
  
  ## An Incompatibility is a set of Terms that are mutually exclusive
  ## Logic: ¬(Term1 ∧ Term2 ∧ ... ∧ TermN)
  ## Or: at least one of the Terms must be false
  ##
  ## Example: "nginx depends on zlib" becomes:
  ##   Incompatibility([Term(nginx, >=1.20), Term(zlib, NOT >=1.0)])
  ##   Meaning: "It's incompatible to have nginx >=1.20 AND NOT have zlib >=1.0"
  Incompatibility* = object
    terms*: seq[Term]
    cause*: IncompatibilityCause
    
    # For error reporting (PubGrub's magic)
    externalContext*: string  ## Human-readable explanation
    fromPackage*: Option[PackageId]  ## Which package caused this
    fromVersion*: Option[SemanticVersion]  ## Which version caused this
  
  ## An Assignment represents a decision made by the solver
  ## It maps packages to specific versions/variants
  Assignment* = object
    package*: PackageId
    version*: SemanticVersion
    variant*: VariantProfile
    
    # Decision level (for backtracking)
    decisionLevel*: int
    
    # Why was this assignment made?
    cause*: Option[Incompatibility]
  
  ## The solver's current state
  ## Tracks all assignments and incompatibilities
  SolverState* = object
    assignments*: Table[PackageId, Assignment]
    incompatibilities*: seq[Incompatibility]
    
    # Current decision level (incremented on each choice)
    decisionLevel*: int
    
    # Packages we've already processed
    processed*: HashSet[PackageId]

# --- String Representations ---

proc `$`*(c: Constraint): string =
  ## String representation of a constraint
  result = $c.versionRange.operator & " " & $c.versionRange.version
  
  if c.variantReq.domains.len > 0:
    result.add(" ")
    for domain, variantDomain in c.variantReq.domains.pairs:
      for flag in variantDomain.flags:
        result.add("+" & domain & ":" & flag & " ")
  
  if c.isNegative:
    result = "NOT (" & result & ")"

proc `$`*(t: Term): string =
  ## String representation of a term
  result = t.package & " " & $t.constraint

proc `$`*(i: Incompatibility): string =
  ## String representation of an incompatibility
  result = "Incompatibility("
  for idx, term in i.terms:
    if idx > 0:
      result.add(" AND ")
    result.add($term)
  result.add(")")
  
  if i.externalContext.len > 0:
    result.add(" [" & i.externalContext & "]")

proc `$`*(a: Assignment): string =
  ## String representation of an assignment
  result = a.package & " = " & $a.version
  if a.variant.domains.len > 0:
    result.add(" " & a.variant.hash)

# --- Hash Functions ---

proc hash*(c: Constraint): Hash =
  ## Hash function for Constraint
  var h: Hash = 0
  h = h !& hash(c.versionRange.operator)
  h = h !& hash($c.versionRange.version)
  h = h !& hash(c.variantReq.hash)
  h = h !& hash(c.isNegative)
  result = !$h

proc hash*(t: Term): Hash =
  ## Hash function for Term
  var h: Hash = 0
  h = h !& hash(t.package)
  h = h !& hash(t.constraint)
  result = !$h

# --- Equality ---

proc `==`*(a, b: Constraint): bool =
  ## Equality for Constraint
  result = a.versionRange.operator == b.versionRange.operator and
           a.versionRange.version == b.versionRange.version and
           a.variantReq.hash == b.variantReq.hash and
           a.isNegative == b.isNegative

proc `==`*(a, b: Term): bool =
  ## Equality for Term
  result = a.package == b.package and a.constraint == b.constraint

# --- Constraint Operations ---

proc isAny*(c: Constraint): bool =
  ## Check if constraint accepts any version
  result = c.versionRange.operator == OpAny and not c.isNegative

proc isEmpty*(c: Constraint): bool =
  ## Check if constraint is empty (no versions satisfy it)
  result = c.isNegative and c.versionRange.operator == OpAny

proc satisfies*(version: SemanticVersion, variant: VariantProfile, constraint: Constraint): bool =
  ## Check if a specific version/variant satisfies a constraint
  
  # Check if negated
  if constraint.isNegative:
    return not satisfies(version, variant, Constraint(
      versionRange: constraint.versionRange,
      variantReq: constraint.variantReq,
      isNegative: false
    ))
  
  # Check version constraint
  if not satisfiesConstraint(version, constraint.versionRange):
    return false
  
  # Check variant requirements
  # For now, we check if all required domains/flags are present
  for domain, variantDomain in constraint.variantReq.domains.pairs:
    if not variant.domains.hasKey(domain):
      return false
    
    # Check if all required flags in this domain are present
    for flag in variantDomain.flags:
      if flag notin variant.domains[domain].flags:
        return false
  
  return true

proc intersect*(a, b: Constraint): Option[Constraint] =
  ## Compute the intersection of two constraints
  ## Returns None if the constraints are incompatible
  ##
  ## This is the heart of constraint solving:
  ## - What is the intersection of >=1.0 and <2.0? (1.0 <= v < 2.0)
  ## - What is the intersection of +wayland and +x11 (if exclusive)? (Empty/Conflict)
  
  # TODO: Implement full constraint intersection logic
  # For now, return a simple implementation
  
  # If either is empty, result is empty
  if a.isEmpty or b.isEmpty:
    return none(Constraint)
  
  # If either is "any", return the other
  if a.isAny:
    return some(b)
  if b.isAny:
    return some(a)
  
  # For now, if constraints are equal, return one of them
  if a == b:
    return some(a)
  
  # Otherwise, we need to compute the actual intersection
  # This requires version range intersection logic
  # TODO: Implement this properly
  return none(Constraint)

proc union*(a, b: Constraint): Option[Constraint] =
  ## Compute the union of two constraints
  ## Returns None if the constraints cannot be unified
  
  # TODO: Implement full constraint union logic
  # For now, return a simple implementation
  
  # If either is "any", result is "any"
  if a.isAny or b.isAny:
    return some(Constraint(
      versionRange: VersionConstraint(operator: OpAny),
      variantReq: newVariantProfile(),
      isNegative: false
    ))
  
  # If constraints are equal, return one of them
  if a == b:
    return some(a)
  
  # Otherwise, we need to compute the actual union
  # TODO: Implement this properly
  return none(Constraint)

# --- Term Operations ---

proc negate*(t: Term): Term =
  ## Negate a term
  ## NOT (P satisfies C) = P satisfies (NOT C)
  result = Term(
    package: t.package,
    constraint: Constraint(
      versionRange: t.constraint.versionRange,
      variantReq: t.constraint.variantReq,
      isNegative: not t.constraint.isNegative
    )
  )

proc isPositive*(t: Term): bool =
  ## Check if term is positive (not negated)
  result = not t.constraint.isNegative

proc isNegative*(t: Term): bool =
  ## Check if term is negative (negated)
  result = t.constraint.isNegative

# --- Incompatibility Operations ---

proc createDependencyIncompatibility*(
  dependent: PackageId,
  dependentVersion: SemanticVersion,
  dependency: PackageId,
  dependencyConstraint: Constraint
): Incompatibility =
  ## Create an incompatibility from a dependency
  ## "Package A version V depends on B with constraint C" becomes:
  ## Incompatibility([Term(A, =V), Term(B, NOT C)])
  ##
  ## Meaning: "It's incompatible to have A=V AND NOT have B satisfying C"
  
  result = Incompatibility(
    terms: @[
      Term(
        package: dependent,
        constraint: Constraint(
          versionRange: VersionConstraint(
            operator: OpExact,
            version: dependentVersion
          ),
          variantReq: newVariantProfile(),
          isNegative: false
        )
      ),
      Term(
        package: dependency,
        constraint: Constraint(
          versionRange: dependencyConstraint.versionRange,
          variantReq: dependencyConstraint.variantReq,
          isNegative: true  # Negated!
        )
      )
    ],
    cause: Dependency,
    externalContext: dependent & " " & $dependentVersion & " depends on " & dependency,
    fromPackage: some(dependent),
    fromVersion: some(dependentVersion)
  )

proc createRootIncompatibility*(package: PackageId, constraint: Constraint): Incompatibility =
  ## Create an incompatibility from a root requirement
  ## "User requires package P with constraint C" becomes:
  ## Incompatibility([Term(P, NOT C)])
  ##
  ## Meaning: "It's incompatible to NOT have P satisfying C"
  
  result = Incompatibility(
    terms: @[
      Term(
        package: package,
        constraint: Constraint(
          versionRange: constraint.versionRange,
          variantReq: constraint.variantReq,
          isNegative: true  # Negated!
        )
      )
    ],
    cause: Root,
    externalContext: "User requires " & package & " " & $constraint,
    fromPackage: some(package),
    fromVersion: none(SemanticVersion)
  )

# --- Solver State Operations ---

proc newSolverState*(): SolverState =
  ## Create a new solver state
  result = SolverState(
    assignments: initTable[PackageId, Assignment](),
    incompatibilities: @[],
    decisionLevel: 0,
    processed: initHashSet[PackageId]()
  )

proc addAssignment*(state: var SolverState, assignment: Assignment) =
  ## Add an assignment to the solver state
  state.assignments[assignment.package] = assignment

proc hasAssignment*(state: SolverState, package: PackageId): bool =
  ## Check if a package has been assigned
  result = state.assignments.hasKey(package)

proc getAssignment*(state: SolverState, package: PackageId): Option[Assignment] =
  ## Get the assignment for a package
  if state.assignments.hasKey(package):
    return some(state.assignments[package])
  else:
    return none(Assignment)

proc addIncompatibility*(state: var SolverState, incomp: Incompatibility) =
  ## Add an incompatibility to the solver state
  state.incompatibilities.add(incomp)

proc incrementDecisionLevel*(state: var SolverState) =
  ## Increment the decision level (when making a choice)
  state.decisionLevel += 1

proc markProcessed*(state: var SolverState, package: PackageId) =
  ## Mark a package as processed
  state.processed.incl(package)

proc isProcessed*(state: SolverState, package: PackageId): bool =
  ## Check if a package has been processed
  result = package in state.processed
