## NIP Namespace Isolation
##
## This module implements the sandboxing and namespace isolation for NIP applications.
## It uses Linux namespaces (User, Mount, PID, Net, IPC) to restrict the application.

import std/[os, posix, strutils, strformat, logging, options]
import nip/manifest_parser

# Linux specific constants (if not in std/posix)
const
  CLONE_NEWNS* = 0x00020000
  CLONE_NEWUTS* = 0x04000000
  CLONE_NEWIPC* = 0x08000000
  CLONE_NEWUSER* = 0x10000000
  CLONE_NEWPID* = 0x20000000
  CLONE_NEWNET* = 0x40000000
  MS_BIND* = 4096
  MS_REC* = 16384
  MS_PRIVATE* = 262144
  MS_RDONLY* = 1

type
  SandboxError* = object of CatchableError

  Launcher* = ref object
    manifest*: PackageManifest
    installDir*: string
    casRoot*: string

proc unshare(flags: cint): cint {.importc: "unshare", header: "<sched.h>".}
proc mount(source, target, filesystemtype: cstring, mountflags: culong, data: cstring): cint {.importc: "mount", header: "<sys/mount.h>".}

proc newLauncher*(manifest: PackageManifest, installDir, casRoot: string): Launcher =
  Launcher(manifest: manifest, installDir: installDir, casRoot: casRoot)

proc setupUserNamespace(l: Launcher) =
  ## Map current user to root inside the namespace
  let uid = getuid()
  let gid = getgid()
  
  let uidMap = fmt"0 {uid} 1"
  let gidMap = fmt"0 {gid} 1"
  
  writeFile("/proc/self/uid_map", uidMap)
  writeFile("/proc/self/setgroups", "deny")
  writeFile("/proc/self/gid_map", gidMap)

proc setupMountNamespace(l: Launcher) =
  ## Setup mount namespace and bind mounts
  
  # 1. Make all mounts private to avoid propagating changes
  if mount("none", "/", "", (MS_REC or MS_PRIVATE).culong, "") != 0:
    raise newException(SandboxError, "Failed to make mounts private")

  # 2. Bind mount the application directory
  # We might want to mount it to a standard location like /app
  # For now, let's just ensure it's accessible.
  
  # 3. Bind mount CAS (Read-Only)
  # This is critical for security and integrity
  if mount(l.casRoot.cstring, l.casRoot.cstring, "none", (MS_BIND or MS_REC).culong, "") != 0:
    raise newException(SandboxError, "Failed to bind mount CAS")
    
  if mount("none", l.casRoot.cstring, "none", (MS_BIND or MS_REC or MS_RDONLY).culong, "") != 0:
    raise newException(SandboxError, "Failed to remount CAS read-only")

  # 4. Handle /proc (needed for PID namespace)
  if mount("proc", "/proc", "proc", 0, "") != 0:
    # This might fail if we are not root or fully unshared yet.
    # In a user namespace, we can mount proc if we are root inside it.
    discard

proc run*(l: Launcher, args: seq[string]) =
  ## Run the application in the sandbox
  info(fmt"Launching {l.manifest.name} in sandbox...")
  
  var flags: cint = 0
  
  # Determine flags based on SandboxConfig
  if l.manifest.sandbox.isSome:
    let sb = l.manifest.sandbox.get()
    
    # Always use User Namespace for rootless execution
    flags = flags or CLONE_NEWUSER
    
    # Always use Mount Namespace for filesystem isolation
    flags = flags or CLONE_NEWNS
    
    # PID Namespace
    if "pid" in sb.namespaces:
      flags = flags or CLONE_NEWPID
      
    # Network Namespace
    if "net" in sb.namespaces:
      flags = flags or CLONE_NEWNET
      
    # IPC Namespace
    if "ipc" in sb.namespaces:
      flags = flags or CLONE_NEWIPC
      
  else:
    # Default strict sandbox
    flags = CLONE_NEWUSER or CLONE_NEWNS or CLONE_NEWPID or CLONE_NEWIPC
  
  # 1. Unshare namespaces
  if unshare(flags) != 0:
    raise newException(SandboxError, "Failed to unshare namespaces: " & $strerror(errno))
    
  # 2. Setup User Mapping (Must be done before other operations that require root)
  if (flags and CLONE_NEWUSER) != 0:
    l.setupUserNamespace()
    
  # 3. Fork for PID namespace (PID 1 inside namespace)
  if (flags and CLONE_NEWPID) != 0:
    let pid = fork()
    if pid < 0:
      raise newException(SandboxError, "Fork failed")
    
    if pid > 0:
      # Parent: wait for child
      var status: cint
      discard waitpid(pid, status, 0)
      return # Exit parent
      
    # Child continues here (as PID 1 in new namespace)
    # We need to mount /proc here
    if mount("proc", "/proc", "proc", 0, "") != 0:
       warn("Failed to mount /proc in new PID namespace")

  # 4. Setup Mounts
  if (flags and CLONE_NEWNS) != 0:
    l.setupMountNamespace()
    
  # 5. Drop Capabilities (TODO)
  # if l.manifest.sandbox.isSome: ...
  
  # 6. Execute Application
  # Find the executable. For now, assume it's in bin/<name>
  let binPath = l.installDir / "bin" / l.manifest.name
  
  # Construct args
  var cargs: seq[cstring] = @[binPath.cstring]
  for arg in args:
    cargs.add(arg.cstring)
  cargs.add(nil)
  
  if execv(binPath.cstring, cast[cstringArray](addr cargs[0])) != 0:
    raise newException(SandboxError, "Failed to exec: " & $strerror(errno))
