## System Integration - NPK Installation & System Management
##
## This module implements the "Physical" layer of the package manager.
## It takes abstract PackageManifests and CAS objects and materializes them
## into the GoboLinux-style directory hierarchy.
##
## Directory Structure:
## - /Programs/<Package>/<Version>/<Hash>/  (Installation Root)
## - /Programs/<Package>/Current             (Symlink to active version)
## - /System/Index/bin/                      (Symlinks to executables)
## - /System/Index/lib/                      (Symlinks to libraries)
##
## Responsibilities:
## 1. File reconstruction from CAS
## 2. Symlink management
## 3. User/Group creation
## 4. Service file generation

import std/[os, posix, strutils, strformat, options, osproc, logging]
import nip/manifest_parser
import nip/cas
import nip/types # For Multihash if needed

type
  SystemIntegrator* = ref object
    casRoot*: string
    programsRoot*: string
    systemIndexRoot*: string
    dryRun*: bool

proc newSystemIntegrator*(casRoot, programsRoot, systemIndexRoot: string, dryRun: bool = false): SystemIntegrator =
  result = SystemIntegrator(
    casRoot: casRoot,
    programsRoot: programsRoot,
    systemIndexRoot: systemIndexRoot,
    dryRun: dryRun
  )

proc log(si: SystemIntegrator, msg: string) =
  if si.dryRun:
    echo "[DRY-RUN] " & msg
  else:
    info(msg)

# ============================================================================
# File Reconstruction
# ============================================================================

proc reconstructFiles(si: SystemIntegrator, manifest: PackageManifest, installDir: string) =
  ## Reconstruct files from CAS chunks into the installation directory
  si.log(fmt"Reconstructing files for {manifest.name} v{manifest.version} in {installDir}")
  
  if not si.dryRun:
    createDir(installDir)

  for file in manifest.files:
    let destPath = installDir / file.path
    let destDir = destPath.parentDir
    
    if not si.dryRun:
      createDir(destDir)
    
    # In a real implementation, we might have multiple chunks per file.
    # For now, we assume 1-to-1 mapping or that CAS handles retrieval transparently.
    # manifest_parser uses string for hash, cas uses Multihash.
    # We assume 'file.hash' is the CAS object hash.
    
    try:
      if not si.dryRun:
        # Retrieve content from CAS
        # Note: cas.retrieveObject takes Multihash
        let content = retrieveObject(Multihash(file.hash), si.casRoot)
        writeFile(destPath, content)
        
        # Set permissions
        # Parse octal string e.g. "755"
        var perms: set[FilePermission] = {}
        if file.permissions.len == 3:
          let user = file.permissions[0].ord - '0'.ord
          let group = file.permissions[1].ord - '0'.ord
          let other = file.permissions[2].ord - '0'.ord
          
          if (user and 4) != 0: perms.incl(fpUserRead)
          if (user and 2) != 0: perms.incl(fpUserWrite)
          if (user and 1) != 0: perms.incl(fpUserExec)
          
          if (group and 4) != 0: perms.incl(fpGroupRead)
          if (group and 2) != 0: perms.incl(fpGroupWrite)
          if (group and 1) != 0: perms.incl(fpGroupExec)
          
          if (other and 4) != 0: perms.incl(fpOthersRead)
          if (other and 2) != 0: perms.incl(fpOthersWrite)
          if (other and 1) != 0: perms.incl(fpOthersExec)
          
          setFilePermissions(destPath, perms)
        
        # Add CAS reference
        # refId = package:version
        let refId = fmt"{manifest.name}:{manifest.version}"
        addReference(si.casRoot, Multihash(file.hash), "npk", refId)

    except Exception as e:
      error(fmt"Failed to reconstruct file {file.path}: {e.msg}")
      raise

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

proc createSymlinks(si: SystemIntegrator, manifest: PackageManifest, installDir: string) =
  ## Create system links in /System/Index
  si.log(fmt"Creating symlinks for {manifest.name}")
  
  # 1. Update 'Current' link
  let packageRoot = si.programsRoot / manifest.name
  let currentLink = packageRoot / "Current"
  
  if not si.dryRun:
    createDir(packageRoot)
    # Atomic symlink update would be better, but for MVP:
    if symlinkExists(currentLink) or fileExists(currentLink):
      removeFile(currentLink)
    createSymlink(installDir, currentLink)

  # 2. Link binaries to /System/Index/bin
  let binDir = installDir / "bin"
  let systemBin = si.systemIndexRoot / "bin"
  
  if dirExists(binDir):
    if not si.dryRun: createDir(systemBin)
    for kind, path in walkDir(binDir):
      if kind == pcFile or kind == pcLinkToFile:
        let filename = path.extractFilename
        let target = systemBin / filename
        si.log(fmt"Linking {filename} -> {target}")
        
        if not si.dryRun:
          if symlinkExists(target) or fileExists(target):
            # Conflict resolution strategy: Overwrite? Warn?
            # For now, overwrite
            removeFile(target)
          # Link to the 'Current' path, not the specific version path, 
          # so upgrades don't break links if 'Current' is updated.
          # Target: /Programs/<Pkg>/Current/bin/<file>
          let persistentPath = currentLink / "bin" / filename
          createSymlink(persistentPath, target)

  # 3. Link libraries to /System/Index/lib
  let libDir = installDir / "lib"
  let systemLib = si.systemIndexRoot / "lib"
  
  if dirExists(libDir):
    if not si.dryRun: createDir(systemLib)
    for kind, path in walkDir(libDir):
      if kind == pcFile or kind == pcLinkToFile:
        let filename = path.extractFilename
        # Only link .so files or similar? Or everything?
        # GoboLinux links everything usually.
        let target = systemLib / filename
        si.log(fmt"Linking {filename} -> {target}")
        
        if not si.dryRun:
          if symlinkExists(target) or fileExists(target):
            removeFile(target)
          let persistentPath = currentLink / "lib" / filename
          createSymlink(persistentPath, target)

  # TODO: Handle share, include, etc.

# ============================================================================
# User/Group Management
# ============================================================================

proc manageUsersGroups(si: SystemIntegrator, manifest: PackageManifest) =
  ## Create users and groups defined in the manifest
  
  # Groups first
  for group in manifest.groups:
    si.log(fmt"Ensuring group exists: {group.name}")
    if not si.dryRun:
      # Check if group exists
      let checkCmd = fmt"getent group {group.name}"
      if execCmd(checkCmd) != 0:
        # Create group
        var cmd = fmt"groupadd {group.name}"
        if group.gid.isSome:
          cmd.add(fmt" -g {group.gid.get()}")
        
        if execCmd(cmd) != 0:
          error(fmt"Failed to create group {group.name}")

  # Users
  for user in manifest.users:
    si.log(fmt"Ensuring user exists: {user.name}")
    if not si.dryRun:
      # Check if user exists
      let checkCmd = fmt"getent passwd {user.name}"
      if execCmd(checkCmd) != 0:
        # Create user
        var cmd = fmt"useradd -m -s {user.shell} -d {user.home}"
        if user.uid.isSome:
          cmd.add(fmt" -u {user.uid.get()}")
        if user.group != "":
          cmd.add(fmt" -g {user.group}")
        cmd.add(fmt" {user.name}")
        
        if execCmd(cmd) != 0:
          error(fmt"Failed to create user {user.name}")

# ============================================================================
# Service Management
# ============================================================================

proc manageServices(si: SystemIntegrator, manifest: PackageManifest) =
  ## Generate and install system service files
  let systemdDir = si.systemIndexRoot / "lib/systemd/system"
  
  if manifest.services.len > 0:
    if not si.dryRun: createDir(systemdDir)
    
    for service in manifest.services:
      let serviceFile = systemdDir / (service.name & ".service")
      si.log(fmt"Installing service: {service.name}")
      
      if not si.dryRun:
        writeFile(serviceFile, service.content)
        
        if service.enabled:
          # Enable service (symlink to multi-user.target.wants usually)
          # For MVP, we just run systemctl enable
          discard execCmd(fmt"systemctl enable {service.name}")

# ============================================================================
# Main Installation Procedure
# ============================================================================

proc installPackage*(si: SystemIntegrator, manifest: PackageManifest) =
  ## Main entry point for installing a package
  info(fmt"Installing {manifest.name} v{manifest.version}")
  
  # 1. Determine installation path
  # /Programs/<Name>/<Version>
  # We might want to include hash in path to allow multiple builds of same version?
  # Task says: /Programs/App/Version/Hash
  let installDir = si.programsRoot / manifest.name / $manifest.version / manifest.artifactHash
  
  if dirExists(installDir):
    warn(fmt"Package version already installed at {installDir}")
    # Proceed anyway to repair/update? Or return?
    # For now, proceed (idempotent)
  
  # 2. Reconstruct files
  si.reconstructFiles(manifest, installDir)
  
  # 3. Create Users/Groups
  si.manageUsersGroups(manifest)
  
  # 4. Create Symlinks (activates the package)
  si.createSymlinks(manifest, installDir)
  
  # 5. Manage Services
  si.manageServices(manifest)
  
# ============================================================================
# Removal Procedure
# ============================================================================

proc removePackage*(si: SystemIntegrator, manifest: PackageManifest) =
  ## Remove an installed package
  info(fmt"Removing {manifest.name} v{manifest.version}")
  
  let installDir = si.programsRoot / manifest.name / $manifest.version / manifest.artifactHash
  let currentLink = si.programsRoot / manifest.name / "Current"
  
  # 1. Stop and Disable Services
  if manifest.services.len > 0:
    for service in manifest.services:
      si.log(fmt"Stopping/Disabling service: {service.name}")
      if not si.dryRun:
        discard execCmd(fmt"systemctl stop {service.name}")
        discard execCmd(fmt"systemctl disable {service.name}")
        
        let serviceFile = si.systemIndexRoot / "lib/systemd/system" / (service.name & ".service")
        if fileExists(serviceFile):
          removeFile(serviceFile)

  # 2. Remove Symlinks from /System/Index
  # We need to know what files were linked.
  # Strategy: Check if 'Current' points to the version we are removing.
  # If so, we should remove the links.
  # If 'Current' points to another version, we should NOT remove links (except maybe cleaning up orphans?)
  
  var isCurrent = false
  if symlinkExists(currentLink):
    let target = expandSymlink(currentLink)
    if target == installDir:
      isCurrent = true
  
  if isCurrent:
    si.log("Removing system symlinks")
    # Binaries
    if dirExists(installDir / "bin"):
      for kind, path in walkDir(installDir / "bin"):
        let filename = path.extractFilename
        let target = si.systemIndexRoot / "bin" / filename
        if not si.dryRun and (symlinkExists(target) or fileExists(target)):
          removeFile(target)
    
    # Libraries
    if dirExists(installDir / "lib"):
      for kind, path in walkDir(installDir / "lib"):
        let filename = path.extractFilename
        let target = si.systemIndexRoot / "lib" / filename
        if not si.dryRun and (symlinkExists(target) or fileExists(target)):
          removeFile(target)
    
    # Remove 'Current' link
    if not si.dryRun:
      removeFile(currentLink)

  # 3. Remove Installation Directory
  if dirExists(installDir):
    si.log(fmt"Removing installation directory: {installDir}")
    if not si.dryRun:
      removeDir(installDir)
      
      # Remove version dir if empty
      let versionDir = installDir.parentDir
      if dirExists(versionDir):
        var versionEmpty = true
        for _ in walkDir(versionDir):
          versionEmpty = false
          break
        if versionEmpty:
          removeDir(versionDir)
      
      # Remove package dir if empty (no other versions)
      let packageDir = si.programsRoot / manifest.name
      if dirExists(packageDir):
        var isEmpty = true
        for _ in walkDir(packageDir):
          isEmpty = false
          break
        if isEmpty:
          removeDir(packageDir)

  # 4. Remove CAS References
  si.log("Removing CAS references")
  if not si.dryRun:
    let refId = fmt"{manifest.name}:{manifest.version}"
    for file in manifest.files:
      removeReference(si.casRoot, Multihash(file.hash), "npk", refId)

  info(fmt"Removal of {manifest.name} complete")

