# SPDX-License-Identifier: LSL-1.0
# Copyright (c) 2026 Markus Maiwald
# Stewardship: Self Sovereign Society Foundation
#
# This file is part of the Nexus Sovereign Core.
# See legal/LICENSE_SOVEREIGN.md for license terms.

## nimpak/generation_filesystem.nim
## Generation-aware filesystem operations for NimPak
##
## This module implements the integration between generation management
## and filesystem operations, providing atomic system state changes.

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

type
  GenerationFilesystemError* = object of CatchableError
    path*: string

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

  GenerationInfo* = object
    id*: string
    timestamp*: times.DateTime
    packages*: seq[string]     ## Package names in this generation
    previous*: string          ## Previous generation ID (empty if first)
    size*: int64

  SymlinkOperation* = object
    source*: string           ## Source file in /Programs
    target*: string           ## Target symlink in /System/Index
    operation*: string        ## "create", "update", "remove"

# =============================================================================
# Generation Manager Creation and Configuration
# =============================================================================

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

proc loadCurrentGeneration*(gm: var GenerationManager): bool =
  ## Load the current generation ID from filesystem
  try:
    let currentGenFile = gm.generationsRoot / "current"
    if fileExists(currentGenFile):
      gm.currentGeneration = readFile(currentGenFile).strip()
      return true
    else:
      # No current generation - this is a fresh system
      gm.currentGeneration = ""
      return true
  except:
    return false

# =============================================================================
# Symlink Operations (defined early for forward references)
# =============================================================================

proc applySymlinkOperationsImpl*(gm: GenerationManager, operations: seq[SymlinkOperation],
                                generationId: string): bool =
  ## Apply symlink operations atomically with backup capability
  try:
    let backupDir = gm.generationsRoot / generationId / "symlink_backup"
    if not gm.dryRun:
      createDir(backupDir)

    # Phase 1: Backup existing symlinks
    for op in operations:
      if symlinkExists(op.target):
        let currentTarget = expandSymlink(op.target)
        if not gm.dryRun:
          let backupFile = backupDir / extractFilename(op.target) & ".backup"
          writeFile(backupFile, currentTarget)
      elif fileExists(op.target):
        # Handle regular files that need to be replaced
        if not gm.dryRun:
          let backupFile = backupDir / extractFilename(op.target) & ".file"
          copyFile(op.target, backupFile)

    # Phase 2: Apply new symlinks
    for op in operations:
      case op.operation:
      of "create", "update":
        let targetDir = parentDir(op.target)

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

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

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

      of "remove":
        if not gm.dryRun:
          if fileExists(op.target) or symlinkExists(op.target):
            removeFile(op.target)
        else:
          echo "DRY RUN: Would remove symlink ", op.target

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

    return true

  except:
    echo "ERROR: Failed to apply symlink operations"
    return false

# =============================================================================
# Generation Creation and Management
# =============================================================================

proc createGeneration*(gm: GenerationManager, generationId: string,
                      packages: seq[string]): bool =
  ## Create a new generation with the specified packages
  try:
    let generationDir = gm.generationsRoot / generationId

    if not gm.dryRun:
      createDir(generationDir)

    # Create generation metadata
    let generationInfo = GenerationInfo(
      id: generationId,
      timestamp: now(),
      packages: packages,
      previous: gm.currentGeneration,
      size: 0  # Will be calculated
    )

    # Save generation metadata as JSON
    let generationJson = %*{
      "id": generationInfo.id,
      "timestamp": $generationInfo.timestamp,
      "packages": generationInfo.packages,
      "previous": generationInfo.previous,
      "size": generationInfo.size
    }

    if not gm.dryRun:
      let generationFile = generationDir / "generation.json"
      writeFile(generationFile, $generationJson)
    else:
      echo "DRY RUN: Would create generation ", generationId

    return true
  except:
    return false

proc switchToGeneration*(gm: var GenerationManager, targetGenerationId: string): bool =
  ## Switch the system to a specific generation atomically
  try:
    let generationDir = gm.generationsRoot / targetGenerationId
    let generationFile = generationDir / "generation.json"

    if not fileExists(generationFile):
      echo "ERROR: Generation not found: ", targetGenerationId
      return false

    # Load generation metadata
    let generationJson = parseJson(readFile(generationFile))
    let packages = generationJson["packages"].getElems().mapIt(it.getStr())

    # Create symlinks for all packages in this generation
    var symlinkOps: seq[SymlinkOperation] = @[]

    for packageName in packages:
      # Find the package directory (assuming latest version for now)
      let packageBaseDir = gm.programsRoot / packageName
      if dirExists(packageBaseDir):
        # Get the latest version directory
        var latestVersion = ""
        for kind, path in walkDir(packageBaseDir):
          if kind == pcDir:
            let version = extractFilename(path)
            if latestVersion.len == 0 or version > latestVersion:
              latestVersion = version

        if latestVersion.len > 0:
          let packageDir = packageBaseDir / latestVersion

          # Scan for binaries to symlink
          let binDir = packageDir / "bin"
          if dirExists(binDir):
            for kind, path in walkDir(binDir):
              if kind == pcFile:
                let fileName = extractFilename(path)
                symlinkOps.add(SymlinkOperation(
                  source: path,
                  target: gm.indexRoot / "bin" / fileName,
                  operation: "create"
                ))

          # Scan for libraries to symlink
          let libDir = packageDir / "lib"
          if dirExists(libDir):
            for kind, path in walkDir(libDir):
              if kind == pcFile and (path.endsWith(".so") or path.contains(".so.")):
                let fileName = extractFilename(path)
                symlinkOps.add(SymlinkOperation(
                  source: path,
                  target: gm.indexRoot / "lib" / fileName,
                  operation: "create"
                ))

    # Apply symlink operations atomically
    if applySymlinkOperationsImpl(gm, symlinkOps, targetGenerationId):
      # Update current generation pointer
      if not gm.dryRun:
        let currentGenFile = gm.generationsRoot / "current"
        writeFile(currentGenFile, targetGenerationId)

      gm.currentGeneration = targetGenerationId
      echo "Successfully switched to generation: ", targetGenerationId
      return true
    else:
      echo "ERROR: Failed to apply symlink operations"
      return false

  except:
    echo "ERROR: Failed to switch to generation: ", targetGenerationId
    return false

proc rollbackToPreviousGeneration*(gm: var GenerationManager): bool =
  ## Rollback to the previous generation
  try:
    if gm.currentGeneration.len == 0:
      echo "ERROR: No current generation to rollback from"
      return false

    # Load current generation metadata
    let currentGenFile = gm.generationsRoot / gm.currentGeneration / "generation.json"
    if not fileExists(currentGenFile):
      echo "ERROR: Current generation metadata not found"
      return false

    let currentGenJson = parseJson(readFile(currentGenFile))
    let previousGenId = currentGenJson["previous"].getStr()

    if previousGenId.len == 0:
      echo "ERROR: No previous generation available for rollback"
      return false

    # Switch to previous generation
    if switchToGeneration(gm, previousGenId):
      echo "Successfully rolled back to generation: ", previousGenId
      return true
    else:
      echo "ERROR: Failed to rollback to previous generation"
      return false

  except:
    echo "ERROR: Failed to rollback generation"
    return false



# =============================================================================
# Generation Information and Utilities
# =============================================================================

proc listGenerations*(gm: GenerationManager): seq[GenerationInfo] =
  ## List all available generations
  var generations: seq[GenerationInfo] = @[]

  try:
    if not dirExists(gm.generationsRoot):
      return generations

    for kind, path in walkDir(gm.generationsRoot):
      if kind == pcDir:
        let generationId = extractFilename(path)
        if generationId != "current":  # Skip the current symlink
          let generationFile = path / "generation.json"
          if fileExists(generationFile):
            try:
              let generationJson = parseJson(readFile(generationFile))
              let genInfo = GenerationInfo(
                id: generationJson["id"].getStr(),
                timestamp: parse(generationJson["timestamp"].getStr(), "yyyy-MM-dd'T'HH:mm:ss'.'fff'Z'", utc()),
                packages: generationJson["packages"].getElems().mapIt(it.getStr()),
                previous: generationJson["previous"].getStr(),
                size: generationJson["size"].getInt()
              )
              generations.add(genInfo)
            except:
              # Skip invalid generation files
              continue
  except:
    # Return empty list on error
    discard

  # Sort by timestamp (newest first)
  generations.sort do (a, b: GenerationInfo) -> int:
    if a.timestamp > b.timestamp: -1
    elif a.timestamp < b.timestamp: 1
    else: 0

  return generations

proc getGenerationInfo*(gm: GenerationManager, generationId: string): GenerationInfo =
  ## Get detailed information about a specific generation
  try:
    let generationFile = gm.generationsRoot / generationId / "generation.json"
    if fileExists(generationFile):
      let generationJson = parseJson(readFile(generationFile))
      return GenerationInfo(
        id: generationJson["id"].getStr(),
        timestamp: parse(generationJson["timestamp"].getStr(), "yyyy-MM-dd'T'HH:mm:ss'.'fff'Z'", utc()),
        packages: generationJson["packages"].getElems().mapIt(it.getStr()),
        previous: generationJson["previous"].getStr(),
        size: generationJson["size"].getInt()
      )
  except:
    discard

  # Return empty generation info on error
  return GenerationInfo(
    id: "",
    timestamp: now(),
    packages: @[],
    previous: "",
    size: 0
  )

proc repairGeneration*(gm: GenerationManager, generationId: string): bool =
  ## Repair a corrupted generation by rebuilding symlinks
  try:
    let generationDir = gm.generationsRoot / generationId
    let symlinkStateFile = generationDir / "symlinks.json"

    if not fileExists(symlinkStateFile):
      echo "ERROR: Generation symlink state not found for repair: ", generationId
      return false

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

    for linkJson in symlinkStateJson["symlinks"].getElems():
      expectedOperations.add(SymlinkOperation(
        source: linkJson["source"].getStr(),
        target: linkJson["target"].getStr(),
        operation: linkJson["operation"].getStr()
      ))

    # Apply the operations to repair the generation
    if applySymlinkOperationsImpl(gm, expectedOperations, generationId):
      echo "Successfully repaired generation: ", generationId
      return true
    else:
      echo "ERROR: Failed to repair generation: ", generationId
      return false

  except:
    echo "ERROR: Failed to repair generation: ", generationId
    return false

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

proc createBootEntry*(gm: GenerationManager, generationId: string, bootDir: string = "/boot"): bool =
  ## Create boot entry for generation selection
  try:
    let bootEntryDir = bootDir / "nexus" / "generations"
    if not gm.dryRun:
      createDir(bootEntryDir)

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

    if not gm.dryRun:
      writeFile(bootEntryFile, bootEntry)
      echo "Created boot entry for generation: ", generationId
    else:
      echo "DRY RUN: Would create boot entry ", bootEntryFile

    return true

  except:
    echo "ERROR: Failed to create boot entry for generation: ", generationId
    return false

proc setDefaultBootGeneration*(gm: GenerationManager, generationId: string, bootDir: string = "/boot"): bool =
  ## Set the default boot generation
  try:
    let defaultBootFile = bootDir / "nexus" / "default_generation"

    if not gm.dryRun:
      writeFile(defaultBootFile, generationId)
      echo "Set default boot generation to: ", generationId
    else:
      echo "DRY RUN: Would set default boot generation to ", generationId

    return true

  except:
    echo "ERROR: Failed to set default boot generation: ", generationId
    return false

# =============================================================================
# CLI Integration Functions
# =============================================================================

proc printGenerationStatus*(gm: GenerationManager) =
  ## Print current generation status
  echo "=== Generation Status ==="
  echo "Current Generation: ", if gm.currentGeneration.len > 0: gm.currentGeneration else: "None"
  echo "Generations Root: ", gm.generationsRoot
  echo "Programs Root: ", gm.programsRoot
  echo "Index Root: ", gm.indexRoot

  let generations = listGenerations(gm)
  echo "Available Generations: ", generations.len

  for gen in generations:
    let marker = if gen.id == gm.currentGeneration: " (current)" else: ""
    echo "  - ", gen.id, " (", gen.packages.len, " packages)", marker

proc printGenerationDetails*(gm: GenerationManager, generationId: string) =
  ## Print detailed information about a generation
  let genInfo = getGenerationInfo(gm, generationId)

  if genInfo.id.len == 0:
    echo "ERROR: Generation not found: ", generationId
    return

  echo "=== Generation Details ==="
  echo "ID: ", genInfo.id
  echo "Timestamp: ", genInfo.timestamp
  echo "Previous: ", if genInfo.previous.len > 0: genInfo.previous else: "None"
  echo "Size: ", genInfo.size, " bytes"
  echo "Packages (", genInfo.packages.len, "):"
  for pkg in genInfo.packages:
    echo "  - ", pkg