## NipCell Fallback for Unresolvable Conflicts
##
## This module implements the NipCell isolation fallback mechanism for the
## NIP dependency resolver. When variant unification fails due to irreconcilable
## conflicts, this module suggests and manages NipCell isolation as an alternative.
##
## **Philosophy:**
## - When the Paradox Engine cannot synthesize a unified solution, we offer
##   isolation as a pragmatic escape hatch
## - NipCells provide separate dependency graphs for conflicting packages
## - Users maintain control over when to use isolation vs. forcing unification
##
## **Requirements:**
## - 10.1: Detect unresolvable conflicts and suggest NipCell isolation
## - 10.2: Create separate NipCells for conflicting packages
## - 10.3: Maintain separate dependency graphs per cell
## - 10.4: Support cell switching for different environments
## - 10.5: Clean up cell-specific packages when removing cells
##
## **Architecture:**
## ```
## ┌─────────────────────────────────────────────────────────────┐
## │  Conflict Detection                                         │
## │  ─────────────────────────────────────────────────────────  │
## │  Detect unresolvable variant conflicts                      │
## │  Analyze conflict severity and isolation candidates         │
## └────────────────────┬────────────────────────────────────────┘
##                      │
##                      v
## ┌─────────────────────────────────────────────────────────────┐
## │  Isolation Suggestion                                       │
## │  ─────────────────────────────────────────────────────────  │
## │  Suggest NipCell isolation with clear explanation           │
## │  Provide actionable commands for user                       │
## └────────────────────┬────────────────────────────────────────┘
##                      │
##                      v
## ┌─────────────────────────────────────────────────────────────┐
## │  Cell Management                                            │
## │  ─────────────────────────────────────────────────────────  │
## │  Create cells, maintain separate graphs, handle switching   │
## └─────────────────────────────────────────────────────────────┘
## ```

import std/[tables, sets, options, sequtils, algorithm, strutils, strformat, times, os, json]
import ./conflict_detection
import ./dependency_graph
import ./solver_types

type
  ## Severity of a conflict for isolation decision
  ConflictSeverity* = enum
    Low,        ## Minor conflict, may be resolvable with flag changes
    Medium,     ## Significant conflict, isolation recommended
    High,       ## Severe conflict, isolation strongly recommended
    Critical    ## Irreconcilable conflict, isolation required

  ## A candidate package for isolation
  IsolationCandidate* = object
    packageName*: string
    conflictingWith*: seq[string]
    severity*: ConflictSeverity
    suggestedCellName*: string
    reason*: string

  ## Suggestion for NipCell isolation
  IsolationSuggestion* = object
    candidates*: seq[IsolationCandidate]
    primaryConflict*: ConflictReport
    suggestedCells*: seq[SuggestedCell]
    explanation*: string
    commands*: seq[string]

  ## A suggested cell configuration
  SuggestedCell* = object
    name*: string
    packages*: seq[string]
    description*: string
    isolationLevel*: string

  ## A NipCell with its own dependency graph
  NipCellGraph* = object
    cellName*: string
    cellId*: string
    graph*: DependencyGraph
    packages*: HashSet[string]
    created*: DateTime
    lastModified*: DateTime
    metadata*: Table[string, string]

  ## Manager for multiple NipCell graphs
  NipCellGraphManager* = ref object
    cells*: Table[string, NipCellGraph]
    activeCell*: Option[string]
    cellRoot*: string
    globalPackages*: HashSet[string]  ## Packages available in all cells

  ## Result of cell creation
  CellCreationResult* = object
    success*: bool
    cellName*: string
    cellId*: string
    error*: string

  ## Result of cell switching
  CellSwitchResult* = object
    success*: bool
    previousCell*: Option[string]
    newCell*: string
    error*: string

# =============================================================================
# Conflict Severity Analysis
# =============================================================================

proc analyzeConflictSeverity*(conflict: ConflictReport): ConflictSeverity =
  ## Analyze the severity of a conflict to determine isolation necessity.
  ##
  ## **Requirements:** 10.1 - Detect unresolvable conflicts
  ##
  ## **Severity Levels:**
  ## - Low: Version conflicts that might be resolved with constraint relaxation
  ## - Medium: Variant conflicts in non-exclusive domains
  ## - High: Variant conflicts in exclusive domains
  ## - Critical: Circular dependencies or fundamental incompatibilities
  
  case conflict.kind:
  of VersionConflict:
    # Version conflicts are usually resolvable
    return Low
  
  of VariantConflict:
    # Check if it's an exclusive domain conflict
    if conflict.details.contains("exclusive"):
      return High
    else:
      return Medium
  
  of CircularDependency:
    # Circular dependencies are critical
    return Critical
  
  of MissingPackage:
    # Missing packages are low severity (just need to find the package)
    return Low
  
  of BuildHashMismatch:
    # Build hash mismatches are medium severity
    return Medium

proc shouldSuggestIsolation*(severity: ConflictSeverity): bool =
  ## Determine if isolation should be suggested based on severity.
  ##
  ## **Requirements:** 10.1 - Suggest NipCell isolation for unresolvable conflicts
  
  case severity:
  of Low:
    return false
  of Medium:
    return true
  of High:
    return true
  of Critical:
    return true

# =============================================================================
# Isolation Candidate Detection
# =============================================================================

proc detectIsolationCandidates*(
  conflicts: seq[ConflictReport]
): seq[IsolationCandidate] =
  ## Detect packages that are good candidates for isolation.
  ##
  ## **Requirements:** 10.1, 10.2 - Detect conflicts and suggest isolation
  ##
  ## **Algorithm:**
  ## 1. Group conflicts by package
  ## 2. Analyze severity of each conflict
  ## 3. Identify packages that would benefit from isolation
  ## 4. Generate suggested cell names
  
  result = @[]
  
  # Group conflicts by package
  var packageConflicts: Table[string, seq[ConflictReport]] = initTable[string, seq[ConflictReport]]()
  
  for conflict in conflicts:
    for pkg in conflict.packages:
      if pkg notin packageConflicts:
        packageConflicts[pkg] = @[]
      packageConflicts[pkg].add(conflict)
  
  # Analyze each package
  for pkg, pkgConflicts in packageConflicts.pairs:
    # Find the most severe conflict
    var maxSeverity = Low
    var conflictingPackages: seq[string] = @[]
    var reasons: seq[string] = @[]
    
    for conflict in pkgConflicts:
      let severity = analyzeConflictSeverity(conflict)
      if severity > maxSeverity:
        maxSeverity = severity
      
      for otherPkg in conflict.packages:
        if otherPkg != pkg and otherPkg notin conflictingPackages:
          conflictingPackages.add(otherPkg)
      
      reasons.add(conflict.details)
    
    # Only suggest isolation for medium+ severity
    if shouldSuggestIsolation(maxSeverity):
      let candidate = IsolationCandidate(
        packageName: pkg,
        conflictingWith: conflictingPackages,
        severity: maxSeverity,
        suggestedCellName: pkg & "-cell",
        reason: reasons.join("; ")
      )
      result.add(candidate)

# =============================================================================
# Isolation Suggestion Generation
# =============================================================================

proc generateIsolationSuggestion*(
  conflict: ConflictReport,
  candidates: seq[IsolationCandidate]
): IsolationSuggestion =
  ## Generate a complete isolation suggestion with commands.
  ##
  ## **Requirements:** 10.1, 10.2 - Suggest NipCell isolation
  ##
  ## **Returns:** Complete suggestion with explanation and CLI commands
  
  var suggestedCells: seq[SuggestedCell] = @[]
  var commands: seq[string] = @[]
  
  # Group candidates by suggested cell
  for candidate in candidates:
    let cell = SuggestedCell(
      name: candidate.suggestedCellName,
      packages: @[candidate.packageName],
      description: fmt"Isolated environment for {candidate.packageName}",
      isolationLevel: if candidate.severity == Critical: "strict" else: "standard"
    )
    suggestedCells.add(cell)
    
    # Generate CLI commands
    commands.add(fmt"nip cell create {candidate.suggestedCellName} --isolation={cell.isolationLevel}")
    commands.add(fmt"nip cell activate {candidate.suggestedCellName}")
    commands.add(fmt"nip install {candidate.packageName}")
  
  # Build explanation
  var explanation = "The following packages have irreconcilable conflicts:\n\n"
  
  for candidate in candidates:
    explanation.add("  • " & candidate.packageName)
    if candidate.conflictingWith.len > 0:
      let conflictList = candidate.conflictingWith.join(", ")
      explanation.add(" (conflicts with: " & conflictList & ")")
    explanation.add("\n")
  
  explanation.add("\nNipCell isolation allows you to install these packages in separate environments,\n")
  explanation.add("each with its own dependency graph. This avoids the conflict while maintaining\n")
  explanation.add("full functionality of each package.\n")
  
  return IsolationSuggestion(
    candidates: candidates,
    primaryConflict: conflict,
    suggestedCells: suggestedCells,
    explanation: explanation,
    commands: commands
  )

proc formatIsolationSuggestion*(suggestion: IsolationSuggestion): string =
  ## Format an isolation suggestion for display.
  ##
  ## **Requirements:** 10.1 - Provide actionable suggestions
  
  result = """
🔀 [IsolationSuggested] NipCell isolation recommended

"""
  result.add(suggestion.explanation)
  result.add("\n💡 Suggested commands:\n\n")
  
  for cmd in suggestion.commands:
    result.add(fmt"  $ {cmd}\n")
  
  result.add("\n📦 Suggested cells:\n\n")
  
  for cell in suggestion.suggestedCells:
    result.add("  • " & cell.name & ": " & cell.description & "\n")
    let pkgList = cell.packages.join(", ")
    result.add("    Packages: " & pkgList & "\n")
    result.add("    Isolation: " & cell.isolationLevel & "\n\n")

# =============================================================================
# NipCell Graph Management
# =============================================================================

proc newNipCellGraph*(cellName: string, cellId: string = ""): NipCellGraph =
  ## Create a new NipCell graph.
  ##
  ## **Requirements:** 10.2, 10.3 - Create cells with separate graphs
  
  let id = if cellId == "": cellName & "-" & $now().toTime().toUnix() else: cellId
  
  result = NipCellGraph(
    cellName: cellName,
    cellId: id,
    graph: newDependencyGraph(),
    packages: initHashSet[string](),
    created: now(),
    lastModified: now(),
    metadata: initTable[string, string]()
  )

proc newNipCellGraphManager*(cellRoot: string = ""): NipCellGraphManager =
  ## Create a new NipCell graph manager.
  ##
  ## **Requirements:** 10.3, 10.4 - Maintain separate graphs and support switching
  
  let root = if cellRoot == "": getHomeDir() / ".nip" / "cells" else: cellRoot
  
  result = NipCellGraphManager(
    cells: initTable[string, NipCellGraph](),
    activeCell: none(string),
    cellRoot: root,
    globalPackages: initHashSet[string]()
  )

proc createCell*(
  manager: NipCellGraphManager,
  cellName: string,
  description: string = ""
): CellCreationResult =
  ## Create a new NipCell with its own dependency graph.
  ##
  ## **Requirements:** 10.2 - Create separate NipCells for conflicting packages
  
  # Check if cell already exists
  if cellName in manager.cells:
    return CellCreationResult(
      success: false,
      cellName: cellName,
      cellId: "",
      error: fmt"Cell '{cellName}' already exists"
    )
  
  # Create new cell graph
  let cellGraph = newNipCellGraph(cellName)
  
  # Add description to metadata
  var graph = cellGraph
  if description != "":
    graph.metadata["description"] = description
  
  # Store in manager
  manager.cells[cellName] = graph
  
  return CellCreationResult(
    success: true,
    cellName: cellName,
    cellId: graph.cellId,
    error: ""
  )

proc deleteCell*(
  manager: NipCellGraphManager,
  cellName: string
): bool =
  ## Delete a NipCell and clean up its packages.
  ##
  ## **Requirements:** 10.5 - Clean up cell-specific packages when removing cells
  
  if cellName notin manager.cells:
    return false
  
  # If this is the active cell, deactivate it
  if manager.activeCell.isSome and manager.activeCell.get() == cellName:
    manager.activeCell = none(string)
  
  # Remove the cell
  manager.cells.del(cellName)
  
  return true

proc switchCell*(
  manager: NipCellGraphManager,
  cellName: string
): CellSwitchResult =
  ## Switch to a different NipCell.
  ##
  ## **Requirements:** 10.4 - Support cell switching
  
  # Check if cell exists
  if cellName notin manager.cells:
    return CellSwitchResult(
      success: false,
      previousCell: manager.activeCell,
      newCell: cellName,
      error: fmt"Cell '{cellName}' not found"
    )
  
  let previousCell = manager.activeCell
  manager.activeCell = some(cellName)
  
  return CellSwitchResult(
    success: true,
    previousCell: previousCell,
    newCell: cellName,
    error: ""
  )

proc getActiveCell*(manager: NipCellGraphManager): Option[string] =
  ## Get the currently active cell name.
  ##
  ## **Requirements:** 10.4 - Support cell switching
  
  return manager.activeCell

proc getCellGraph*(
  manager: NipCellGraphManager,
  cellName: string
): Option[NipCellGraph] =
  ## Get the dependency graph for a specific cell.
  ##
  ## **Requirements:** 10.3 - Maintain separate dependency graphs per cell
  
  if cellName in manager.cells:
    return some(manager.cells[cellName])
  return none(NipCellGraph)

proc getActiveCellGraph*(manager: NipCellGraphManager): Option[NipCellGraph] =
  ## Get the dependency graph for the active cell.
  ##
  ## **Requirements:** 10.3, 10.4 - Maintain graphs and support switching
  
  if manager.activeCell.isSome:
    return manager.getCellGraph(manager.activeCell.get())
  return none(NipCellGraph)

proc listCells*(manager: NipCellGraphManager): seq[string] =
  ## List all available cells.
  ##
  ## **Requirements:** 10.4 - Support cell management
  
  result = @[]
  for cellName in manager.cells.keys:
    result.add(cellName)
  result.sort()

# =============================================================================
# Package Management in Cells
# =============================================================================

proc addPackageToCell*(
  manager: NipCellGraphManager,
  cellName: string,
  packageName: string
): bool =
  ## Add a package to a cell's dependency graph.
  ##
  ## **Requirements:** 10.3 - Maintain separate dependency graphs per cell
  
  if cellName notin manager.cells:
    return false
  
  manager.cells[cellName].packages.incl(packageName)
  manager.cells[cellName].lastModified = now()
  
  return true

proc removePackageFromCell*(
  manager: NipCellGraphManager,
  cellName: string,
  packageName: string
): bool =
  ## Remove a package from a cell's dependency graph.
  ##
  ## **Requirements:** 10.5 - Clean up cell-specific packages
  
  if cellName notin manager.cells:
    return false
  
  if packageName notin manager.cells[cellName].packages:
    return false
  
  manager.cells[cellName].packages.excl(packageName)
  manager.cells[cellName].lastModified = now()
  
  return true

proc getCellPackages*(
  manager: NipCellGraphManager,
  cellName: string
): seq[string] =
  ## Get all packages in a cell.
  ##
  ## **Requirements:** 10.3 - Maintain separate dependency graphs per cell
  
  if cellName notin manager.cells:
    return @[]
  
  result = toSeq(manager.cells[cellName].packages)
  result.sort()

proc isPackageInCell*(
  manager: NipCellGraphManager,
  cellName: string,
  packageName: string
): bool =
  ## Check if a package is in a specific cell.
  ##
  ## **Requirements:** 10.3 - Maintain separate dependency graphs per cell
  
  if cellName notin manager.cells:
    return false
  
  return packageName in manager.cells[cellName].packages

# =============================================================================
# Conflict-Triggered Fallback
# =============================================================================

proc checkForIsolationFallback*(
  conflicts: seq[ConflictReport]
): Option[IsolationSuggestion] =
  ## Check if conflicts warrant NipCell isolation and generate suggestion.
  ##
  ## **Requirements:** 10.1 - Detect unresolvable conflicts and suggest isolation
  ##
  ## **Returns:** Isolation suggestion if warranted, None otherwise
  
  if conflicts.len == 0:
    return none(IsolationSuggestion)
  
  # Detect isolation candidates
  let candidates = detectIsolationCandidates(conflicts)
  
  if candidates.len == 0:
    return none(IsolationSuggestion)
  
  # Generate suggestion based on the first (primary) conflict
  let suggestion = generateIsolationSuggestion(conflicts[0], candidates)
  
  return some(suggestion)

proc handleUnresolvableConflict*(
  manager: NipCellGraphManager,
  conflict: ConflictReport,
  autoCreate: bool = false
): tuple[suggestion: IsolationSuggestion, cellsCreated: seq[string]] =
  ## Handle an unresolvable conflict by suggesting or creating cells.
  ##
  ## **Requirements:** 10.1, 10.2 - Detect conflicts and create cells
  ##
  ## **Parameters:**
  ## - manager: The cell graph manager
  ## - conflict: The conflict to handle
  ## - autoCreate: If true, automatically create suggested cells
  ##
  ## **Returns:** Tuple of suggestion and list of created cell names
  
  let candidates = detectIsolationCandidates(@[conflict])
  let suggestion = generateIsolationSuggestion(conflict, candidates)
  
  var cellsCreated: seq[string] = @[]
  
  if autoCreate:
    for cell in suggestion.suggestedCells:
      let createResult = manager.createCell(cell.name, cell.description)
      if createResult.success:
        cellsCreated.add(cell.name)
  
  return (suggestion: suggestion, cellsCreated: cellsCreated)

# =============================================================================
# Cell Serialization (for persistence)
# =============================================================================

proc toJson*(cell: NipCellGraph): JsonNode =
  ## Serialize a NipCell graph to JSON.
  ##
  ## **Requirements:** 10.3 - Maintain separate dependency graphs
  
  result = %*{
    "cellName": cell.cellName,
    "cellId": cell.cellId,
    "packages": toSeq(cell.packages),
    "created": $cell.created,
    "lastModified": $cell.lastModified,
    "metadata": cell.metadata
  }

proc fromJson*(json: JsonNode): NipCellGraph =
  ## Deserialize a NipCell graph from JSON.
  ##
  ## **Requirements:** 10.3 - Maintain separate dependency graphs
  
  result = NipCellGraph(
    cellName: json["cellName"].getStr(),
    cellId: json["cellId"].getStr(),
    graph: newDependencyGraph(),
    packages: initHashSet[string](),
    created: now(),  # Would need proper parsing
    lastModified: now(),
    metadata: initTable[string, string]()
  )
  
  for pkg in json["packages"]:
    result.packages.incl(pkg.getStr())
  
  for key, value in json["metadata"].pairs:
    result.metadata[key] = value.getStr()
