## nimpak/filesystem.nim
## GoboLinux-style filesystem management with generation integration
##
## This module implements the filesystem operations for NimPak, including:
## - GoboLinux-style /Programs/App/Version directory structure
## - Atomic symlink management in /System/Index
## - Generation-aware filesystem operations
## - Boot integration for generation selection

import std/[os, strutils, times, json, tables, sequtils, algorithm, osproc]
import ./types_fixed

type
  FilesystemError* = object of types_fixed.NimPakError
    path*: string

  EnhancedFilesystemManager* = object
    programsRoot*: string      ## /Programs - Package installation directory
    indexRoot*: string         ## /System/Index - Symlink directory
    generationsRoot*: string   ## /System/Generations - Generation metadata
    currentGeneration*: string ## Current active generation ID
    dryRun*: bool             ## Dry run mode for testing

  GenerationFilesystem* = object
    generation*: Generation
    symlinkMap*: Table[string, string]  ## target -> source mapping
    backupPath*: string                 ## Backup location for rollback

# =============================================================================
# FilesystemManager Creation and Configuration
# =============================================================================

proc newEnhancedFilesystemManager*(programsRoot: string = "/Programs",
                                  indexRoot: string = "/System/Index",
                                  generationsRoot: string = "/System/Generations",
                                  dryRun: bool = false): EnhancedFilesystemManager =
  ## Create a new EnhancedFilesystemManager with specified paths
  EnhancedFilesystemManager(
    programsRoot: programsRoot,
    indexRoot: indexRoot,
    generationsRoot: generationsRoot,
    currentGeneration: "",  # Will be loaded from filesystem
    dryRun: dryRun
  )

proc loadCurrentGeneration*(fm: var EnhancedFilesystemManager): Result[void, FilesystemError] =
  ## Load the current generation ID from filesystem
  try:
    let currentGenFile = fm.generationsRoot / "current"
    if fileExists(currentGenFile):
      fm.currentGeneration = readFile(currentGenFile).strip()
    else:
      # No current generation - this is a fresh system
      fm.currentGeneration = ""

    return ok[void, FilesystemError]()
  except IOError as e:
    return err[void, FilesystemError](FilesystemError(
      code: FileReadError,
      msg: "Failed to load current generation: " & e.msg,
      path: fm.generationsRoot / "current"
    ))

# =============================================================================
# Package Installation with Generation Integration
# =============================================================================

proc installPackage*(fm: EnhancedFilesystemManager, pkg: Fragment, generation: Generation): Result[InstallLocation, FilesystemError] =
  ## Install a package to the filesystem with generation tracking
  try:
    let programDir = fm.programsRoot / pkg.id.name / pkg.id.version

    # Create program directory structure
    if not fm.dryRun:
      createDir(programDir)
      createDir(programDir / "bin")
      createDir(programDir / "lib")
      createDir(programDir / "share")
      createDir(programDir / "etc")

    # Generate symlinks for this package
    var indexLinks: seq[SymlinkPair] = @[]

    # Scan for binaries to symlink
    let binDir = programDir / "bin"
    if dirExists(binDir):
      for file in walkDir(binDir):
        if file.kind == pcFile:
          let fileName = extractFilename(file.path)
          indexLinks.add(SymlinkPair(
            source: file.path,
            target: fm.indexRoot / "bin" / fileName
          ))

    # Scan for libraries to symlink
    let libDir = programDir / "lib"
    if dirExists(libDir):
      for file in walkDir(libDir):
        if file.kind == pcFile and (file.path.endsWith(".so") or file.path.contains(".so.")):
          let fileName = extractFilename(file.path)
          indexLinks.add(SymlinkPair(
            source: file.path,
            target: fm.indexRoot / "lib" / fileName
          ))

    # Scan for shared data to symlink
    let shareDir = programDir / "share"
    if dirExists(shareDir):
      for subdir in walkDir(shareDir):
        if subdir.kind == pcDir:
          let dirName = extractFilename(subdir.path)
          indexLinks.add(SymlinkPair(
            source: subdir.path,
            target: fm.indexRoot / "share" / dirName
          ))

    let location = InstallLocation(
      programDir: programDir,
      indexLinks: indexLinks
    )

    return ok[InstallLocation, FilesystemError](location)

  except OSError as e:
    return err[InstallLocation, FilesystemError](FilesystemError(
      code: FileWriteError,
      msg: "Failed to install package: " & e.msg,
      path: fm.programsRoot / pkg.id.name / pkg.id.version
    ))

# =============================================================================
# Atomic Symlink Management
# =============================================================================

proc createSymlinks*(fm: EnhancedFilesystemManager, location: InstallLocation, generation: Generation): Result[void, FilesystemError] =
  ## Create symlinks for package installation with generation tracking
  try:
    for link in location.indexLinks:
      let targetDir = parentDir(link.target)

      if not fm.dryRun:
        # Ensure target directory exists
        if not dirExists(targetDir):
          createDir(targetDir)

        # Create the symlink (remove existing if present)
        if symlinkExists(link.target) or fileExists(link.target):
          removeFile(link.target)

        createSymlink(link.source, link.target)
      else:
        echo "DRY RUN: Would create symlink " & link.source & " -> " & link.target

    return ok[void, FilesystemError]()

  except OSError as e:
    return err[void, FilesystemError](FilesystemError(
      code: FileWriteError,
      msg: "Failed to create symlinks: " & e.msg,
      path: location.programDir
    ))

proc atomicSymlinkUpdate*(fm: EnhancedFilesystemManager, updates: seq[SymlinkPair], generation: Generation): Result[void, FilesystemError] =
  ## Atomically update symlinks with generation tracking and rollback capability
  try:
    # Create backup of current symlink state
    let backupDir = fm.generationsRoot / generation.id / "symlink_backup"
    if not fm.dryRun:
      createDir(backupDir)

    var backupData: seq[SymlinkPair] = @[]

    # Phase 1: Backup existing symlinks
    for update in updates:
      if symlinkExists(update.target):
        let currentTarget = expandSymlink(update.target)
        backupData.add(SymlinkPair(
          source: currentTarget,
          target: update.target
        ))

        if not fm.dryRun:
          # Save backup information
          let backupFile = backupDir / extractFilename(update.target) & ".backup"
          writeFile(backupFile, currentTarget)
      elif fileExists(update.target):
        # Handle regular files that need to be replaced
        let backupFile = backupDir / extractFilename(update.target) & ".file"
        if not fm.dryRun:
          copyFile(update.target, backupFile)

    # Phase 2: Apply new symlinks atomically
    for update in updates:
      let targetDir = parentDir(update.target)

      if not fm.dryRun:
        # Ensure target directory exists
        if not dirExists(targetDir):
          createDir(targetDir)

        # Remove existing file/symlink
        if fileExists(update.target) or symlinkExists(update.target):
          removeFile(update.target)

        # Create new symlink
        createSymlink(update.source, update.target)
      else:
        echo "DRY RUN: Would update symlink " & update.source & " -> " & update.target

    # Phase 3: Record generation symlink state
    if not fm.dryRun:
      let symlinkStateFile = fm.generationsRoot / generation.id / "symlinks.json"
      let symlinkState = %*{
        "generation": generation.id,
        "timestamp": $generation.timestamp,
        "symlinks": updates.mapIt(%*{
          "source": it.source,
          "target": it.target
        }),
        "backup_location": backupDir
      }
      writeFile(symlinkStateFile, $symlinkState)

    return ok[void, FilesystemError]()

  except OSError as e:
    # Attempt rollback on failure
    discard rollbackSymlinks(fm, backupData)
    return err[void, FilesystemError](FilesystemError(
      code: FileWriteError,
      msg: "Failed to update symlinks atomically: " & e.msg,
      path: fm.indexRoot
    ))

proc rollbackSymlinks*(fm: EnhancedFilesystemManager, backupData: seq[SymlinkPair]): Result[void, FilesystemError] =
  ## Rollback symlinks to previous state
  try:
    for backup in backupData:
      if not fm.dryRun:
        # Remove current symlink
        if fileExists(backup.target) or symlinkExists(backup.target):
          removeFile(backup.target)

        # Restore original symlink
        createSymlink(backup.source, backup.target)
      else:
        echo "DRY RUN: Would rollback symlink " & backup.source & " -> " & backup.target

    return ok[void, FilesystemError]()

  except OSError as e:
    return err[void, FilesystemError](FilesystemError(
      code: FileWriteError,
      msg: "Failed to rollback symlinks: " & e.msg,
      path: fm.indexRoot
    ))

# =============================================================================
# Generation Switching and Management
# =============================================================================

proc switchToGeneration*(fm: var EnhancedFilesystemManager, targetGeneration: Generation): Result[void, FilesystemError] =
  ## Switch the system to a specific generation atomically
  try:
    let generationDir = fm.generationsRoot / targetGeneration.id
    let symlinkStateFile = generationDir / "symlinks.json"

    if not fileExists(symlinkStateFile):
      return err[void, FilesystemError](FilesystemError(
        code: PackageNotFound,
        msg: "Generation symlink state not found: " & targetGeneration.id,
        path: symlinkStateFile
      ))

    # Load generation symlink state
    let symlinkStateJson = parseJson(readFile(symlinkStateFile))
    var targetSymlinks: seq[SymlinkPair] = @[]

    for linkJson in symlinkStateJson["symlinks"].getElems():
      targetSymlinks.add(SymlinkPair(
        source: linkJson["source"].getStr(),
        target: linkJson["target"].getStr()
      ))

    # Perform atomic symlink update to target generation
    let updateResult = atomicSymlinkUpdate(fm, targetSymlinks, targetGeneration)
    if updateResult.isErr:
      return updateResult

    # Update current generation pointer
    if not fm.dryRun:
      let currentGenFile = fm.generationsRoot / "current"
      writeFile(currentGenFile, targetGeneration.id)

    fm.currentGeneration = targetGeneration.id

    return ok[void, FilesystemError]()

  except JsonParsingError as e:
    return err[void, FilesystemError](FilesystemError(
      code: InvalidMetadata,
      msg: "Failed to parse generation metadata: " & e.msg,
      path: fm.generationsRoot / targetGeneration.id
    ))
  except IOError as e:
    return err[void, FilesystemError](FilesystemError(
      code: FileReadError,
      msg: "Failed to switch generation: " & e.msg,
      path: fm.generationsRoot / targetGeneration.id
    ))

proc rollbackToPreviousGeneration*(fm: var EnhancedFilesystemManager): Result[Generation, FilesystemError] =
  ## Rollback to the previous generation
  try:
    if fm.currentGeneration.len == 0:
      return err[Generation, FilesystemError](FilesystemError(
        code: PackageNotFound,
        msg: "No current generation to rollback from",
        path: fm.generationsRoot
      ))

    # Load current generation metadata
    let currentGenFile = fm.generationsRoot / fm.currentGeneration / "generation.json"
    if not fileExists(currentGenFile):
      return err[Generation, FilesystemError](FilesystemError(
        code: PackageNotFound,
        msg: "Current generation metadata not found",
        path: currentGenFile
      ))

    let currentGenJson = parseJson(readFile(currentGenFile))
    let previousGenId = currentGenJson.getOrDefault("previous")

    if previousGenId.isNil or previousGenId.getStr().len == 0:
      return err[Generation, FilesystemError](FilesystemError(
        code: PackageNotFound,
        msg: "No previous generation available for rollback",
        path: currentGenFile
      ))

    # Load previous generation
    let previousGenFile = fm.generationsRoot / previousGenId.getStr() / "generation.json"
    let previousGenJson = parseJson(readFile(previousGenFile))

    let previousGeneration = Generation(
      id: previousGenId.getStr(),
      timestamp: previousGenJson["timestamp"].getStr().parseTime("yyyy-MM-dd'T'HH:mm:ss'.'fff'Z'", utc()),
      packages: previousGenJson["packages"].getElems().mapIt(PackageId(
        name: it["name"].getStr(),
        version: it["version"].getStr(),
        stream: parseEnum[PackageStream](it["stream"].getStr())
      )),
      previous: if previousGenJson.hasKey("previous") and not previousGenJson["previous"].isNil:
                  some(previousGenJson["previous"].getStr())
                else:
                  none(string),
      size: previousGenJson["size"].getInt()
    )

    # Switch to previous generation
    let switchResult = switchToGeneration(fm, previousGeneration)
    if switchResult.isErr:
      return err[Generation, FilesystemError](switchResult.getError())

    return ok[Generation, FilesystemError](previousGeneration)

  except JsonParsingError as e:
    return err[Generation, FilesystemError](FilesystemError(
      code: InvalidMetadata,
      msg: "Failed to parse generation metadata: " & e.msg,
      path: fm.generationsRoot
    ))
  except IOError as e:
    return err[Generation, FilesystemError](FilesystemError(
      code: FileReadError,
      msg: "Failed to rollback generation: " & e.msg,
      path: fm.generationsRoot
    ))

# =============================================================================
# Generation Export and Import
# =============================================================================

proc exportGeneration*(fm: EnhancedFilesystemManager, generation: Generation, exportPath: string): Result[void, FilesystemError] =
  ## Export a generation for system migration
  try:
    let generationDir = fm.generationsRoot / generation.id

    if not dirExists(generationDir):
      return err[void, FilesystemError](FilesystemError(
        code: PackageNotFound,
        msg: "Generation directory not found: " & generation.id,
        path: generationDir
      ))

    # Create export archive
    let exportCmd = "tar -czf " & exportPath & " -C " & generationDir & " ."
    let result = execProcess(exportCmd, options = {poUsePath})

    if result.exitCode != 0:
      return err[void, FilesystemError](FilesystemError(
        code: FileWriteError,
        msg: "Failed to export generation: " & result.output,
        path: exportPath
      ))

    return ok[void, FilesystemError]()

  except OSError as e:
    return err[void, FilesystemError](FilesystemError(
      code: FileWriteError,
      msg: "Failed to export generation: " & e.msg,
      path: exportPath
    ))

proc importGeneration*(fm: EnhancedFilesystemManager, importPath: string, newGenerationId: string): Result[Generation, FilesystemError] =
  ## Import a generation from archive
  try:
    let generationDir = fm.generationsRoot / newGenerationId

    if dirExists(generationDir):
      return err[Generation, FilesystemError](FilesystemError(
        code: FileWriteError,
        msg: "Generation already exists: " & newGenerationId,
        path: generationDir
      ))

    # Create generation directory
    if not fm.dryRun:
      createDir(generationDir)

    # Extract archive
    let extractCmd = "tar -xzf " & importPath & " -C " & generationDir
    let result = execProcess(extractCmd, options = {poUsePath})

    if result.exitCode != 0:
      return err[Generation, FilesystemError](FilesystemError(
        code: FileReadError,
        msg: "Failed to import generation: " & result.output,
        path: importPath
      ))

    # Load generation metadata
    let generationFile = generationDir / "generation.json"
    let generationJson = parseJson(readFile(generationFile))

    let importedGeneration = Generation(
      id: newGenerationId,  # Use new ID
      timestamp: now(),     # Update timestamp
      packages: generationJson["packages"].getElems().mapIt(PackageId(
        name: it["name"].getStr(),
        version: it["version"].getStr(),
        stream: parseEnum[PackageStream](it["stream"].getStr())
      )),
      previous: some(fm.currentGeneration),  # Link to current generation
      size: generationJson["size"].getInt()
    )

    return ok[Generation, FilesystemError](importedGeneration)

  except JsonParsingError as e:
    return err[Generation, FilesystemError](FilesystemError(
      code: InvalidMetadata,
      msg: "Failed to parse imported generation: " & e.msg,
      path: importPath
    ))
  except IOError as e:
    return err[Generation, FilesystemError](FilesystemError(
      code: FileReadError,
      msg: "Failed to import generation: " & e.msg,
      path: importPath
    ))

# =============================================================================
# Generation Repair and Recovery
# =============================================================================

proc repairGeneration*(fm: EnhancedFilesystemManager, generation: Generation): Result[void, FilesystemError] =
  ## Repair a corrupted generation by rebuilding symlinks
  try:
    let generationDir = fm.generationsRoot / generation.id
    let symlinkStateFile = generationDir / "symlinks.json"

    if not fileExists(symlinkStateFile):
      return err[void, FilesystemError](FilesystemError(
        code: PackageNotFound,
        msg: "Generation symlink state not found for repair: " & generation.id,
        path: symlinkStateFile
      ))

    # Load expected symlink state
    let symlinkStateJson = parseJson(readFile(symlinkStateFile))
    var expectedSymlinks: seq[SymlinkPair] = @[]

    for linkJson in symlinkStateJson["symlinks"].getElems():
      expectedSymlinks.add(SymlinkPair(
        source: linkJson["source"].getStr(),
        target: linkJson["target"].getStr()
      ))

    # Verify and repair each symlink
    var repairedCount = 0
    for expected in expectedSymlinks:
      let needsRepair = if symlinkExists(expected.target):
        expandSymlink(expected.target) != expected.source
      else:
        true  # Missing symlink needs repair

      if needsRepair:
        if not fm.dryRun:
          # Remove incorrect symlink/file
          if fileExists(expected.target) or symlinkExists(expected.target):
            removeFile(expected.target)

          # Ensure target directory exists
          let targetDir = parentDir(expected.target)
          if not dirExists(targetDir):
            createDir(targetDir)

          # Create correct symlink
          createSymlink(expected.source, expected.target)
        else:
          echo "DRY RUN: Would repair symlink " & expected.source & " -> " & expected.target

        repairedCount += 1

    if repairedCount > 0:
      echo "Repaired " & $repairedCount & " symlinks in generation " & generation.id
    else:
      echo "Generation " & generation.id & " is healthy - no repairs needed"

    return ok[void, FilesystemError]()

  except JsonParsingError as e:
    return err[void, FilesystemError](FilesystemError(
      code: InvalidMetadata,
      msg: "Failed to parse generation metadata for repair: " & e.msg,
      path: generationDir
    ))
  except OSError as e:
    return err[void, FilesystemError](FilesystemError(
      code: FileWriteError,
      msg: "Failed to repair generation: " & e.msg,
      path: generationDir
    ))

# =============================================================================
# Boot Integration Support
# =============================================================================

proc createBootEntry*(fm: EnhancedFilesystemManager, generation: Generation, bootDir: string = "/boot"): Result[void, FilesystemError] =
  ## Create boot entry for generation selection
  try:
    let bootEntryDir = bootDir / "nexus" / "generations"
    if not fm.dryRun:
      createDir(bootEntryDir)

    let bootEntryFile = bootEntryDir / generation.id & ".conf"
    let bootEntry = """
title NexusOS Generation """ & generation.id & """
version """ & generation.id & """
linux /nexus/kernel
initrd /nexus/initrd
options nexus.generation=""" & generation.id & """ root=LABEL=nexus-root
"""

    if not fm.dryRun:
      writeFile(bootEntryFile, bootEntry)
    else:
      echo "DRY RUN: Would create boot entry " & bootEntryFile

    return ok[void, FilesystemError]()

  except IOError as e:
    return err[void, FilesystemError](FilesystemError(
      code: FileWriteError,
      msg: "Failed to create boot entry: " & e.msg,
      path: bootDir
    ))

proc setDefaultBootGeneration*(fm: EnhancedFilesystemManager, generation: Generation, bootDir: string = "/boot"): Result[void, FilesystemError] =
  ## Set the default boot generation
  try:
    let defaultBootFile = bootDir / "nexus" / "default_generation"

    if not fm.dryRun:
      writeFile(defaultBootFile, generation.id)
    else:
      echo "DRY RUN: Would set default boot generation to " & generation.id

    return ok[void, FilesystemError]()

  except IOError as e:
    return err[void, FilesystemError](FilesystemError(
      code: FileWriteError,
      msg: "Failed to set default boot generation: " & e.msg,
      path: bootDir
    ))

# =============================================================================
# Utility Functions
# =============================================================================

proc getGenerationFilesystemInfo*(fm: EnhancedFilesystemManager, generation: Generation): Result[GenerationFilesystem, FilesystemError] =
  ## Get detailed filesystem information for a generation
  try:
    let generationDir = fm.generationsRoot / generation.id
    let symlinkStateFile = generationDir / "symlinks.json"

    if not fileExists(symlinkStateFile):
      return err[GenerationFilesystem, FilesystemError](FilesystemError(
        code: PackageNotFound,
        msg: "Generation filesystem state not found: " & generation.id,
        path: symlinkStateFile
      ))

    let symlinkStateJson = parseJson(readFile(symlinkStateFile))
    var symlinkMap: Table[string, string] = initTable[string, string]()

    for linkJson in symlinkStateJson["symlinks"].getElems():
      let target = linkJson["target"].getStr()
      let source = linkJson["source"].getStr()
      symlinkMap[target] = source

    let backupPath = symlinkStateJson.getOrDefault("backup_location")
    let backupLocation = if backupPath.isNil: "" else: backupPath.getStr()

    let genFs = GenerationFilesystem(
      generation: generation,
      symlinkMap: symlinkMap,
      backupPath: backupLocation
    )

    return ok[GenerationFilesystem, FilesystemError](genFs)

  except JsonParsingError as e:
    return err[GenerationFilesystem, FilesystemError](FilesystemError(
      code: InvalidMetadata,
      msg: "Failed to parse generation filesystem info: " & e.msg,
      path: generationDir
    ))
  except IOError as e:
    return err[GenerationFilesystem, FilesystemError](FilesystemError(
      code: FileReadError,
      msg: "Failed to load generation filesystem info: " & e.msg,
      path: generationDir
    ))