## NIP Installer - User Application Installation
##
## This module handles the installation of NIP packages (User Applications)
## into the user's home directory (~/.local/share/nexus/nips).
## It integrates with the desktop environment via XDG standards.

import std/[os, strutils, strformat, options, logging, sequtils, osproc]
import nip/manifest_parser
import nip/cas
import nip/types

type
  NipInstaller* = ref object
    casRoot*: string
    installRoot*: string # ~/.local/share/nexus/nips
    appsRoot*: string    # ~/.local/share/applications
    iconsRoot*: string   # ~/.local/share/icons
    dryRun*: bool

proc newNipInstaller*(casRoot: string, dryRun: bool = false): NipInstaller =
  let home = getHomeDir()
  result = NipInstaller(
    casRoot: casRoot,
    installRoot: home / ".local/share/nexus/nips",
    appsRoot: home / ".local/share/applications",
    iconsRoot: home / ".local/share/icons",
    dryRun: dryRun
  )

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

# ============================================================================
# File Reconstruction (Shared Logic - could be refactored)
# ============================================================================

proc reconstructFiles(ni: NipInstaller, manifest: PackageManifest, installDir: string) =
  ## Reconstruct files from CAS
  ni.log(fmt"Reconstructing files for {manifest.name} in {installDir}")
  
  if not ni.dryRun:
    createDir(installDir)

  for file in manifest.files:
    let destPath = installDir / file.path
    let destDir = destPath.parentDir
    
    if not ni.dryRun:
      createDir(destDir)
      try:
        # Retrieve content from CAS
        let content = retrieveObject(Multihash(file.hash), ni.casRoot)
        writeFile(destPath, content)
        
        # Set permissions (basic)
        # TODO: Parse permissions string properly
        setFilePermissions(destPath, {fpUserRead, fpUserWrite, fpUserExec})
        
        # Add CAS reference
        let refId = fmt"{manifest.name}:{manifest.version}"
        addReference(ni.casRoot, Multihash(file.hash), "nip", refId)
        
      except Exception as e:
        error(fmt"Failed to reconstruct file {file.path}: {e.msg}")
        raise

# ============================================================================
# Desktop Integration
# ============================================================================

proc generateDesktopFile(ni: NipInstaller, manifest: PackageManifest) =
  ## Generate .desktop file for the application
  if manifest.desktop.isNone:
    return

  let dt = manifest.desktop.get()
  let desktopFile = ni.appsRoot / (manifest.name & ".desktop")
  
  ni.log(fmt"Generating desktop entry: {desktopFile}")
  
  if not ni.dryRun:
    createDir(ni.appsRoot)
    
    var content = "[Desktop Entry]\n"
    content.add("Type=Application\n")
    content.add(fmt"Name={dt.displayName}\n")
    
    # Exec command
    # We use 'nip run' to launch the app in its sandbox
    # TODO: Ensure 'nip' is in PATH or use absolute path
    content.add(fmt"Exec=nip run {manifest.name}\n")
    
    if dt.icon.isSome:
      content.add(fmt"Icon={dt.icon.get()}\n")
      
    if dt.categories.len > 0:
      content.add("Categories=" & dt.categories.join(";") & ";\n")
      
    if dt.keywords.len > 0:
      content.add("Keywords=" & dt.keywords.join(";") & ";\n")
      
    if dt.mimeTypes.len > 0:
      content.add("MimeType=" & dt.mimeTypes.join(";") & ";\n")
      
    content.add(fmt"Terminal={dt.terminal}\n")
    content.add(fmt"StartupNotify={dt.startupNotify}\n")
    
    if dt.startupWMClass.isSome:
      content.add(fmt"StartupWMClass={dt.startupWMClass.get()}\n")
    
    writeFile(desktopFile, content)

proc installIcons(ni: NipInstaller, manifest: PackageManifest, installDir: string) =
  ## Install icons to ~/.local/share/icons
  if manifest.desktop.isNone: return
  let dt = manifest.desktop.get()
  if dt.icon.isNone: return
  
  let iconName = dt.icon.get()
  # Check if icon is a file path in the package
  # We assume standard paths like share/icons/hicolor/48x48/apps/icon.png
  # Or just a file at the root?
  # For MVP, let's look for the file in the installDir
  
  # Heuristic: If iconName has an extension, it's a file.
  if iconName.endsWith(".png") or iconName.endsWith(".svg"):
    let srcPath = installDir / iconName
    if fileExists(srcPath):
      # Determine destination based on size/type?
      # For MVP, put in hicolor/48x48/apps/ if png, scalable/apps/ if svg
      # Better: Just put in ~/.local/share/icons/hicolor/48x48/apps/ for now
      # Or ~/.local/share/icons/ if we don't know size
      
      let destDir = if iconName.endsWith(".svg"):
        ni.iconsRoot / "hicolor/scalable/apps"
      else:
        ni.iconsRoot / "hicolor/48x48/apps"
        
      let destPath = destDir / (manifest.name & iconName.extractFilename.splitFile.ext)
      
      ni.log(fmt"Installing icon to {destPath}")
      if not ni.dryRun:
        createDir(destDir)
        copyFile(srcPath, destPath)

proc updateDesktopDb(ni: NipInstaller) =
  ## Update desktop database
  ni.log("Updating desktop database")
  if not ni.dryRun:
    discard execCmd("update-desktop-database " & ni.appsRoot)

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

proc installNip*(ni: NipInstaller, manifest: PackageManifest) =
  ## Install a NIP package
  info(fmt"Installing NIP: {manifest.name} v{manifest.version}")
  
  # 1. Determine paths
  let installDir = ni.installRoot / manifest.name / $manifest.version / manifest.artifactHash
  let currentLink = ni.installRoot / manifest.name / "Current"
  
  # 2. Reconstruct files
  ni.reconstructFiles(manifest, installDir)
  
  # 2.1 Write Manifest
  # We need the manifest at runtime for sandboxing configuration
  if not ni.dryRun:
    writeFile(installDir / "manifest.kdl", serializeManifestToKDL(manifest))
  
  # 3. Update 'Current' symlink
  if not ni.dryRun:
    if symlinkExists(currentLink) or fileExists(currentLink):
      removeFile(currentLink)
    createSymlink(installDir, currentLink)
    
  # 4. Desktop Integration
  ni.generateDesktopFile(manifest)
  ni.installIcons(manifest, installDir)
  ni.updateDesktopDb()
  
  info(fmt"NIP installation of {manifest.name} complete")

# ============================================================================
# Removal Procedure
# ============================================================================

proc removeNip*(ni: NipInstaller, manifest: PackageManifest) =
  ## Remove a NIP package
  info(fmt"Removing NIP: {manifest.name}")
  
  let installDir = ni.installRoot / manifest.name / $manifest.version / manifest.artifactHash
  let currentLink = ni.installRoot / manifest.name / "Current"
  let desktopFile = ni.appsRoot / (manifest.name & ".desktop")
  
  # 1. Remove Desktop Entry
  if fileExists(desktopFile):
    ni.log("Removing desktop entry")
    if not ni.dryRun:
      removeFile(desktopFile)
      
  # 1.5 Remove Icons (Best effort)
  # We'd need to know where we put them.
  # For now, check standard locations
  let iconPng = ni.iconsRoot / "hicolor/48x48/apps" / (manifest.name & ".png")
  if fileExists(iconPng):
    ni.log("Removing icon (png)")
    if not ni.dryRun: removeFile(iconPng)
    
  let iconSvg = ni.iconsRoot / "hicolor/scalable/apps" / (manifest.name & ".svg")
  if fileExists(iconSvg):
    ni.log("Removing icon (svg)")
    if not ni.dryRun: removeFile(iconSvg)

  # 2. Remove 'Current' link if it points to this version
  if symlinkExists(currentLink):
    if expandSymlink(currentLink) == installDir:
      ni.log("Removing Current symlink")
      if not ni.dryRun:
        removeFile(currentLink)
        
  # 3. Remove Installation Directory
  if dirExists(installDir):
    ni.log("Removing installation directory")
    if not ni.dryRun:
      removeDir(installDir)
      
      # Clean up parent dirs
      let versionDir = installDir.parentDir
      if dirExists(versionDir) and toSeq(walkDir(versionDir)).len == 0:
        removeDir(versionDir)
        
      let packageDir = ni.installRoot / manifest.name
      if dirExists(packageDir) and toSeq(walkDir(packageDir)).len == 0:
        removeDir(packageDir)
        
  # 4. Remove CAS References
  ni.log("Removing CAS references")
  if not ni.dryRun:
    let refId = fmt"{manifest.name}:{manifest.version}"
    for file in manifest.files:
      removeReference(ni.casRoot, Multihash(file.hash), "nip", refId)
      
  ni.updateDesktopDb()
  info(fmt"NIP removal of {manifest.name} complete")
