## variant_switch.nim
## Commands for switching between installed package variants
## Fixed: All critical bugs including non-atomic symlink updates and error handling

import std/[os, strformat, strutils, algorithm]
import ../config

type
  VariantInfo* = object
    casHash*: string
    descriptor*: string
    path*: string
    isActive*: bool

proc listInstalledVariants*(packageName: string, version: string, programsDir: string): seq[VariantInfo] =
  ## List all installed variants of a package version
  result = @[]

  let versionDir = programsDir / packageName / version
  if not dirExists(versionDir):
    return

  # Find the Current symlink to determine active variant
  let currentLink = versionDir / "Current"
  var activePath = ""

  # FIX: Handle broken symlinks
  if symlinkExists(currentLink):
    try:
      activePath = expandSymlink(currentLink)
      # Verify the target actually exists
      if not dirExists(activePath):
        echo "⚠️  Warning: Current symlink is broken, will be fixed on next switch"
        activePath = ""
    except OSError:
      echo "⚠️  Warning: Current symlink is broken, will be fixed on next switch"
      activePath = ""

  # Scan for all CAS hash directories
  for entry in walkDir(versionDir):
    if entry.kind == pcDir and entry.path.extractFilename().startsWith("blake"):
      let casHash = entry.path.extractFilename()

      # Scan for variant descriptors inside CAS directory
      for variantEntry in walkDir(entry.path):
        if variantEntry.kind == pcDir:
          let descriptor = variantEntry.path.extractFilename()
          let isActive = (variantEntry.path == activePath)

          result.add(VariantInfo(
            casHash: casHash,
            descriptor: descriptor,
            path: variantEntry.path,
            isActive: isActive
          ))

proc switchVariant*(packageName: string, version: string, targetDescriptor: string, programsDir: string): bool =
  ## Switch to a different variant by updating the Current symlink
  ## FIX: Atomic symlink update to prevent crash window
  let versionDir = programsDir / packageName / version
  if not dirExists(versionDir):
    echo fmt"❌ Package {packageName} {version} not found"
    return false

  # Find the target variant
  var targetPath = ""
  for entry in walkDir(versionDir):
    if entry.kind == pcDir and entry.path.extractFilename().startsWith("blake"):
      for variantEntry in walkDir(entry.path):
        if variantEntry.kind == pcDir and variantEntry.path.extractFilename() == targetDescriptor:
          targetPath = variantEntry.path
          break
      if targetPath != "":
        break

  if targetPath == "":
    echo fmt"❌ Variant '{targetDescriptor}' not found"
    return false

  # Verify target path exists
  if not dirExists(targetPath):
    echo fmt"❌ Target variant path does not exist: {targetPath}"
    return false

  # FIX: Atomic symlink update using temp + rename
  let currentLink = versionDir / "Current"
  let tempLink = versionDir / ".Current.tmp"

  try:
    # Save previous for rollback
    # FIX: Handle broken Current symlink
    if symlinkExists(currentLink):
      try:
        let previousPath = expandSymlink(currentLink)
        if dirExists(previousPath):  # Only save if valid
          let previousLink = versionDir / "Previous"
          if symlinkExists(previousLink):
            removeFile(previousLink)
          createSymlink(previousPath, previousLink)
      except OSError:
        echo "⚠️  Warning: Could not save previous variant (broken symlink)"

    # Create new symlink atomically
    # 1. Create temp symlink
    if symlinkExists(tempLink) or fileExists(tempLink):
      removeFile(tempLink)
    createSymlink(targetPath, tempLink)

    # 2. Atomic rename (this is the atomic operation)
    if symlinkExists(currentLink) or fileExists(currentLink):
      removeFile(currentLink)
    moveFile(tempLink, currentLink)

    echo fmt"✅ Switched to variant: {targetDescriptor}"
    return true

  except OSError as e:
    echo fmt"❌ Failed to switch variant: {e.msg}"
    # Cleanup temp file if it exists
    if symlinkExists(tempLink) or fileExists(tempLink):
      try:
        removeFile(tempLink)
      except:
        discard
    return false

proc showActiveVariant*(packageName: string, version: string, programsDir: string): bool =
  ## Show the currently active variant
  let versionDir = programsDir / packageName / version
  if not dirExists(versionDir):
    echo fmt"❌ Package {packageName} {version} not found"
    return false

  let currentLink = versionDir / "Current"

  # FIX: Check existence before checking if it's a symlink
  if not fileExists(currentLink) and not symlinkExists(currentLink):
    echo fmt"❌ No active variant set"
    return false

  if not symlinkExists(currentLink):
    echo fmt"❌ Current is not a symlink (corrupted)"
    return false

  # FIX: Handle broken symlinks
  try:
    let activePath = expandSymlink(currentLink)

    # Verify target exists
    if not dirExists(activePath):
      echo fmt"❌ Active variant symlink is broken"
      echo fmt"   Target does not exist: {activePath}"
      return false

    let descriptor = activePath.extractFilename()
    let casHash = activePath.parentDir().extractFilename()

    # FIX: Bounds check for hash slicing
    let shortHash = if casHash.len > 20: casHash[0..19] & "..." else: casHash

    echo fmt"🎯 Active variant: {descriptor}"
    echo fmt"   CAS: {shortHash}"
    echo fmt"   Path: {activePath}"

    return true

  except OSError as e:
    echo fmt"❌ Failed to read active variant: {e.msg}"
    return false

proc rollbackVariant*(packageName: string, version: string, programsDir: string): bool =
  ## Rollback to the previous variant
  ## FIX: Atomic symlink update
  let versionDir = programsDir / packageName / version
  if not dirExists(versionDir):
    echo fmt"❌ Package {packageName} {version} not found"
    return false

  let previousLink = versionDir / "Previous"
  if not symlinkExists(previousLink):
    echo fmt"❌ No previous variant to rollback to"
    return false

  # FIX: Handle broken Previous symlink
  var previousPath = ""
  try:
    previousPath = expandSymlink(previousLink)
    if not dirExists(previousPath):
      echo fmt"❌ Previous variant symlink is broken"
      return false
  except OSError as e:
    echo fmt"❌ Failed to read previous variant: {e.msg}"
    return false

  let descriptor = previousPath.extractFilename()

  # FIX: Atomic swap using temp symlinks
  let currentLink = versionDir / "Current"
  let tempCurrent = versionDir / ".Current.tmp"
  let tempPrevious = versionDir / ".Previous.tmp"

  try:
    # Get current path (if exists)
    var currentPath = ""
    if symlinkExists(currentLink):
      try:
        currentPath = expandSymlink(currentLink)
      except OSError:
        discard  # Current is broken, that's ok

    # Create temp symlinks
    if symlinkExists(tempCurrent) or fileExists(tempCurrent):
      removeFile(tempCurrent)
    createSymlink(previousPath, tempCurrent)

    if currentPath != "" and dirExists(currentPath):
      if symlinkExists(tempPrevious) or fileExists(tempPrevious):
        removeFile(tempPrevious)
      createSymlink(currentPath, tempPrevious)

    # Atomic rename operations
    if symlinkExists(currentLink) or fileExists(currentLink):
      removeFile(currentLink)
    moveFile(tempCurrent, currentLink)

    if currentPath != "" and dirExists(currentPath):
      if symlinkExists(previousLink):
        removeFile(previousLink)
      moveFile(tempPrevious, previousLink)
    else:
      # No valid previous, just remove the link
      if symlinkExists(previousLink):
        removeFile(previousLink)

    echo fmt"⏪ Rolled back to variant: {descriptor}"
    return true

  except OSError as e:
    echo fmt"❌ Failed to rollback: {e.msg}"
    # Cleanup temp files
    for temp in [tempCurrent, tempPrevious]:
      if symlinkExists(temp) or fileExists(temp):
        try:
          removeFile(temp)
        except:
          discard
    return false

# CLI command implementations

proc variantsListCommand*(packageName: string): int =
  ## List all installed variants of a package
  let cfg = loadConfig()

  # Find all versions of the package
  let packageDir = cfg.programsDir / packageName
  if not dirExists(packageDir):
    echo fmt"❌ Package '{packageName}' not installed"
    return 1

  echo fmt"📦 {packageName} - Installed Variants:"
  echo ""

  var foundAny = false

  # FIX: Sort versions properly instead of just taking first
  var versions: seq[string] = @[]
  for versionEntry in walkDir(packageDir):
    if versionEntry.kind == pcDir:
      versions.add(versionEntry.path.extractFilename())

  # Sort versions (simple lexicographic for now)
  versions.sort(system.cmp, Descending)

  for version in versions:
    let variants = listInstalledVariants(packageName, version, cfg.programsDir)

    if variants.len > 0:
      foundAny = true
      echo fmt"Version {version}:"
      for variant in variants:
        let activeMarker = if variant.isActive: " [ACTIVE]" else: ""
        let icon = if variant.isActive: "🎯" else: "✅"
        # FIX: Bounds check for hash slicing
        let shortHash = if variant.casHash.len > 20:
                          variant.casHash[0..19] & "..."
                        else:
                          variant.casHash
        echo fmt"  {icon} {variant.descriptor} ({shortHash}){activeMarker}"
      echo ""

  if not foundAny:
    echo "No variants found"
    return 1

  return 0

proc switchCommand*(packageName: string, targetDescriptor: string): int =
  ## Switch to a different variant
  let cfg = loadConfig()

  # Find the latest version (or could specify version)
  let packageDir = cfg.programsDir / packageName
  if not dirExists(packageDir):
    echo fmt"❌ Package '{packageName}' not installed"
    return 1

  # FIX: Get latest version properly (sorted)
  var versions: seq[string] = @[]
  for versionEntry in walkDir(packageDir):
    if versionEntry.kind == pcDir:
      versions.add(versionEntry.path.extractFilename())

  if versions.len == 0:
    echo fmt"❌ No versions found for {packageName}"
    return 1

  # Sort and take latest
  versions.sort(system.cmp, Descending)
  let latestVersion = versions[0]

  echo fmt"🔄 Switching {packageName} {latestVersion} to variant: {targetDescriptor}"
  echo ""

  if switchVariant(packageName, latestVersion, targetDescriptor, cfg.programsDir):
    return 0
  else:
    return 1

proc activeCommand*(packageName: string): int =
  ## Show the active variant
  let cfg = loadConfig()

  let packageDir = cfg.programsDir / packageName
  if not dirExists(packageDir):
    echo fmt"❌ Package '{packageName}' not installed"
    return 1

  # FIX: Get latest version properly (sorted)
  var versions: seq[string] = @[]
  for versionEntry in walkDir(packageDir):
    if versionEntry.kind == pcDir:
      versions.add(versionEntry.path.extractFilename())

  if versions.len == 0:
    echo fmt"❌ No versions found for {packageName}"
    return 1

  versions.sort(system.cmp, Descending)
  let latestVersion = versions[0]

  echo fmt"📦 {packageName} {latestVersion}"
  echo ""

  if showActiveVariant(packageName, latestVersion, cfg.programsDir):
    return 0
  else:
    return 1

proc rollbackCommand*(packageName: string): int =
  ## Rollback to previous variant
  let cfg = loadConfig()

  let packageDir = cfg.programsDir / packageName
  if not dirExists(packageDir):
    echo fmt"❌ Package '{packageName}' not installed"
    return 1

  # FIX: Get latest version properly (sorted)
  var versions: seq[string] = @[]
  for versionEntry in walkDir(packageDir):
    if versionEntry.kind == pcDir:
      versions.add(versionEntry.path.extractFilename())

  if versions.len == 0:
    echo fmt"❌ No versions found for {packageName}"
    return 1

  versions.sort(system.cmp, Descending)
  let latestVersion = versions[0]

  echo fmt"⏪ Rolling back {packageName} {latestVersion}"
  echo ""

  if rollbackVariant(packageName, latestVersion, cfg.programsDir):
    return 0
  else:
    return 1
