## NIP Manifest Schema - User Application Format
##
## **Purpose:**
## Defines the NIP (Nexus Installation Package) manifest schema for user applications.
## NIP packages are sandboxed desktop applications with namespace isolation.
##
## **Design Principles:**
## - Desktop integration (icons, .desktop files, MIME types)
## - Namespace isolation with permission controls
## - User-level installation (no root required)
## - Sandboxed execution environment
## - Ed25519 signature support
##
## **Requirements:**
## - Requirement 4.1: manifest.kdl, metadata.json, desktop integration files, CAS chunks, signature
## - Requirement 4.2: app name, version, permissions, namespace config, CAS chunk references
## - Requirement 4.3: .desktop file, icons, MIME type associations
## - Requirement 6.2: KDL format with chunk references by xxh3 hash
## - Requirement 6.5: exact versions and build hashes for dependencies

import std/[times, options, strutils, tables]
import nip/manifest_parser

type
  # ============================================================================
  # NIP-Specific Types
  # ============================================================================

  NIPManifest* = object
    ## Complete NIP manifest for user applications
    # Core identity
    name*: string
    version*: SemanticVersion
    buildDate*: DateTime

    # Application metadata
    metadata*: AppInfo
    provenance*: ProvenanceInfo
    buildConfig*: BuildConfiguration

    # CAS chunk references
    casChunks*: seq[ChunkReference]

    # Desktop integration
    desktop*: DesktopMetadata

    # Namespace isolation and permissions
    namespace*: NamespaceConfig

    # Integrity
    buildHash*: string      ## xxh3-128 hash of build configuration
    signature*: SignatureInfo

  AppInfo* = object
    ## Application metadata
    description*: string
    homepage*: Option[string]
    license*: string
    author*: Option[string]
    maintainer*: Option[string]
    tags*: seq[string]
    category*: Option[string]  ## Application category (e.g., "Graphics", "Network")

  ProvenanceInfo* = object
    ## Complete provenance tracking
    source*: string          ## Source URL or repository
    sourceHash*: string      ## xxh3-128 hash of source
    upstream*: Option[string]  ## Upstream project URL
    buildTimestamp*: DateTime
    builder*: Option[string]   ## Who built this package

  BuildConfiguration* = object
    ## Build configuration for reproducibility
    configureFlags*: seq[string]
    compilerFlags*: seq[string]
    compilerVersion*: string
    targetArchitecture*: string
    libc*: string            ## musl, glibc
    allocator*: string       ## jemalloc, tcmalloc, default
    buildSystem*: string     ## cmake, meson, autotools, etc.

  ChunkReference* = object
    ## Reference to a CAS chunk
    hash*: string            ## xxh3-128 hash
    size*: int64
    chunkType*: ChunkType
    path*: string            ## Relative path in package

  ChunkType* = enum
    ## Type of chunk content
    Binary, Library, Runtime, Config, Data

  DesktopMetadata* = object
    ## Desktop integration metadata
    desktopFile*: DesktopFileSpec
    icons*: seq[IconSpec]
    mimeTypes*: seq[string]
    appId*: string           ## Unique application ID (e.g., "org.mozilla.firefox")

  DesktopFileSpec* = object
    ## .desktop file specification
    name*: string            ## Display name
    genericName*: Option[string]
    comment*: Option[string]
    exec*: string            ## Executable command
    icon*: string            ## Icon name
    terminal*: bool
    categories*: seq[string]
    keywords*: seq[string]

  IconSpec* = object
    ## Icon specification
    size*: int               ## Icon size (e.g., 48, 64, 128)
    path*: string            ## Path to icon file in package
    format*: string          ## Icon format (png, svg)

  NamespaceConfig* = object
    ## Namespace isolation configuration
    namespaceType*: string   ## "user", "strict", "none"
    permissions*: Permissions
    mounts*: seq[Mount]

  Permissions* = object
    ## Application permissions
    network*: bool
    gpu*: bool
    audio*: bool
    camera*: bool
    microphone*: bool
    filesystem*: seq[FilesystemAccess]
    dbus*: DBusAccess

  FilesystemAccess* = object
    ## Filesystem access permission
    path*: string
    mode*: AccessMode

  AccessMode* = enum
    ## Filesystem access mode
    ReadOnly, ReadWrite, Create

  DBusAccess* = object
    ## D-Bus access permissions
    session*: seq[string]    ## Session bus names
    system*: seq[string]     ## System bus names
    own*: seq[string]        ## Bus names to own

  Mount* = object
    ## Filesystem mount specification
    source*: string
    target*: string
    mountType*: MountType
    readOnly*: bool

  MountType* = enum
    ## Mount type
    Bind, Tmpfs, Devtmpfs

  SignatureInfo* = object
    ## Ed25519 signature information
    algorithm*: string       ## "ed25519"
    keyId*: string
    signature*: string       ## Base64-encoded signature

  # ============================================================================
  # Error Types
  # ============================================================================

  NIPError* = object of CatchableError
    code*: NIPErrorCode
    context*: string

  NIPErrorCode* = enum
    InvalidManifest,
    MissingField,
    InvalidHash,
    InvalidSignature,
    InvalidPermissions

# ============================================================================
# KDL Parsing - Minimal implementation to expose gaps via tests
# ============================================================================

proc parseNIPManifest*(kdl: string): NIPManifest =
  ## Parse NIP manifest from KDL format
  ##
  ## **Requirements:**
  ## - Requirement 4.2: Parse app name, version, permissions, namespace config, CAS chunks
  ## - Requirement 4.3: Parse .desktop file, icons, MIME type associations
  ## - Requirement 6.2: Validate chunk references by xxh3 hash
  ## - Requirement 6.5: Parse exact versions and build hashes for dependencies

  # Simple line-based parser for the KDL format we generate
  # This works because we control the generation format

  var lines = kdl.splitLines()
  var name = ""
  var version = SemanticVersion(major: 0, minor: 0, patch: 0)
  var buildDate = now()
  var buildHash = ""

  var metadata = AppInfo(description: "", license: "", tags: @[])
  var provenance = ProvenanceInfo(source: "", sourceHash: "", buildTimestamp: now())
  var buildConfig = BuildConfiguration(
    configureFlags: @[], compilerFlags: @[],
    compilerVersion: "", targetArchitecture: "",
    libc: "", allocator: "", buildSystem: ""
  )
  var casChunks: seq[ChunkReference] = @[]
  var desktop = DesktopMetadata(
    desktopFile: DesktopFileSpec(name: "", exec: "", icon: "", terminal: false, categories: @[], keywords: @[]),
    icons: @[], mimeTypes: @[], appId: ""
  )
  var namespace = NamespaceConfig(
    namespaceType: "user",
    permissions: Permissions(
      network: false, gpu: false, audio: false, camera: false, microphone: false,
      filesystem: @[], dbus: DBusAccess(session: @[], system: @[], own: @[])
    ),
    mounts: @[]
  )
  var signature = SignatureInfo(algorithm: "", keyId: "", signature: "")

  # Helper to extract quoted string
  proc extractQuoted(line: string): string =
    let start = line.find("\"")
    if start >= 0:
      let endIdx = line.find("\"", start + 1)
      if endIdx > start:
        return line[start+1..<endIdx]
    return ""

  # Helper to extract boolean
  proc extractBool(line: string): bool =
    return "true" in line.toLowerAscii()

  # Helper to extract integer
  proc extractInt(line: string): int =
    let parts = line.split()
    for part in parts:
      try:
        return parseInt(part)
      except:
        discard
    return 0

  var i = 0
  var currentSection = ""
  var currentChunk: ChunkReference
  var currentIcon: IconSpec
  var currentMount: Mount
  var currentFsAccess: FilesystemAccess
  var skipSectionReset = false  # Flag to skip section reset for nested blocks

  while i < lines.len:
    let line = lines[i].strip()

    # Parse app name
    if line.startsWith("app \""):
      name = extractQuoted(line)

    # Parse top-level fields
    elif line.startsWith("version \""):
      let vstr = extractQuoted(line)
      let parts = vstr.split(".")
      if parts.len >= 3:
        version = SemanticVersion(
          major: parseInt(parts[0]),
          minor: parseInt(parts[1]),
          patch: parseInt(parts[2])
        )

    elif line.startsWith("build_date \""):
      let dateStr = extractQuoted(line)
      try:
        buildDate = parse(dateStr, "yyyy-MM-dd'T'HH:mm:ss'Z'")
      except:
        buildDate = now()

    elif line.startsWith("build_hash \""):
      buildHash = extractQuoted(line)

    # Track sections
    elif line == "metadata {":
      currentSection = "metadata"
    elif line == "provenance {":
      currentSection = "provenance"
    elif line == "build_config {":
      currentSection = "build_config"
    elif line == "cas_chunks {":
      currentSection = "cas_chunks"
    elif line == "desktop {":
      currentSection = "desktop"
    elif line == "desktop_file {":
      currentSection = "desktop_file"
    elif line == "icons {":
      currentSection = "icons"
    elif line == "namespace {":
      currentSection = "namespace"
    elif line == "permissions {":
      currentSection = "permissions"
    elif line == "filesystem {":
      currentSection = "filesystem"
    elif line == "dbus {":
      currentSection = "dbus"
    elif line == "mounts {":
      currentSection = "mounts"
    elif line == "signature {":
      currentSection = "signature"

    # Parse section content
    elif currentSection == "metadata":
      if line.startsWith("description \""):
        metadata.description = extractQuoted(line)
      elif line.startsWith("license \""):
        metadata.license = extractQuoted(line)
      elif line.startsWith("homepage \""):
        metadata.homepage = some(extractQuoted(line))
      elif line.startsWith("author \""):
        metadata.author = some(extractQuoted(line))
      elif line.startsWith("maintainer \""):
        metadata.maintainer = some(extractQuoted(line))
      elif line.startsWith("category \""):
        metadata.category = some(extractQuoted(line))
      elif line.startsWith("tags \""):
        let tagsStr = extractQuoted(line)
        metadata.tags = tagsStr.split()

    elif currentSection == "provenance":
      if line.startsWith("source \""):
        provenance.source = extractQuoted(line)
      elif line.startsWith("source_hash \""):
        provenance.sourceHash = extractQuoted(line)
      elif line.startsWith("upstream \""):
        provenance.upstream = some(extractQuoted(line))
      elif line.startsWith("build_timestamp \""):
        let dateStr = extractQuoted(line)
        try:
          provenance.buildTimestamp = parse(dateStr, "yyyy-MM-dd'T'HH:mm:ss'Z'")
        except:
          provenance.buildTimestamp = now()
      elif line.startsWith("builder \""):
        provenance.builder = some(extractQuoted(line))

    elif currentSection == "build_config":
      if line.startsWith("configure_flags \""):
        let flagsStr = extractQuoted(line)
        buildConfig.configureFlags = flagsStr.split()
      elif line.startsWith("compiler_flags \""):
        let flagsStr = extractQuoted(line)
        buildConfig.compilerFlags = flagsStr.split()
      elif line.startsWith("compiler_version \""):
        buildConfig.compilerVersion = extractQuoted(line)
      elif line.startsWith("target_architecture \""):
        buildConfig.targetArchitecture = extractQuoted(line)
      elif line.startsWith("libc \""):
        buildConfig.libc = extractQuoted(line)
      elif line.startsWith("allocator \""):
        buildConfig.allocator = extractQuoted(line)
      elif line.startsWith("build_system \""):
        buildConfig.buildSystem = extractQuoted(line)

    elif currentSection == "cas_chunks":
      if line.startsWith("chunk \""):
        currentChunk = ChunkReference(hash: extractQuoted(line), size: 0, chunkType: Binary, path: "")
      elif line.startsWith("size "):
        currentChunk.size = extractInt(line).int64
      elif line.startsWith("type \""):
        let typeStr = extractQuoted(line)
        case typeStr:
        of "binary": currentChunk.chunkType = Binary
        of "library": currentChunk.chunkType = Library
        of "runtime": currentChunk.chunkType = Runtime
        of "config": currentChunk.chunkType = Config
        of "data": currentChunk.chunkType = Data
        else: currentChunk.chunkType = Binary
      elif line.startsWith("path \""):
        currentChunk.path = extractQuoted(line)
      elif line == "}":
        if currentChunk.hash.len > 0:
          casChunks.add(currentChunk)
          currentChunk = ChunkReference(hash: "", size: 0, chunkType: Binary, path: "")
          skipSectionReset = true  # Don't reset section, we're still in cas_chunks

    elif currentSection == "desktop":
      if line.startsWith("app_id \""):
        desktop.appId = extractQuoted(line)
      elif line.startsWith("mime_types \""):
        let mimeStr = extractQuoted(line)
        desktop.mimeTypes = mimeStr.split(";")

    elif currentSection == "desktop_file":
      if line.startsWith("name \""):
        desktop.desktopFile.name = extractQuoted(line)
      elif line.startsWith("generic_name \""):
        desktop.desktopFile.genericName = some(extractQuoted(line))
      elif line.startsWith("comment \""):
        desktop.desktopFile.comment = some(extractQuoted(line))
      elif line.startsWith("exec \""):
        desktop.desktopFile.exec = extractQuoted(line)
      elif line.startsWith("icon \""):
        desktop.desktopFile.icon = extractQuoted(line)
      elif line.startsWith("terminal "):
        desktop.desktopFile.terminal = extractBool(line)
      elif line.startsWith("categories \""):
        let catStr = extractQuoted(line)
        desktop.desktopFile.categories = catStr.split(";")
      elif line.startsWith("keywords \""):
        let kwStr = extractQuoted(line)
        desktop.desktopFile.keywords = kwStr.split(";")

    elif currentSection == "icons":
      if line.startsWith("icon {"):
        currentIcon = IconSpec(size: 0, path: "", format: "")
      elif line.startsWith("size "):
        currentIcon.size = extractInt(line)
      elif line.startsWith("path \""):
        currentIcon.path = extractQuoted(line)
      elif line.startsWith("format \""):
        currentIcon.format = extractQuoted(line)
      elif line == "}" and currentIcon.path.len > 0:
        # This closes an individual icon block
        desktop.icons.add(currentIcon)
        currentIcon = IconSpec(size: 0, path: "", format: "")
        skipSectionReset = true  # Don't reset section, we're still in icons

    elif currentSection == "namespace":
      if line.startsWith("type \""):
        namespace.namespaceType = extractQuoted(line)

    elif currentSection == "permissions":
      if line.startsWith("network "):
        namespace.permissions.network = extractBool(line)
      elif line.startsWith("gpu "):
        namespace.permissions.gpu = extractBool(line)
      elif line.startsWith("audio "):
        namespace.permissions.audio = extractBool(line)
      elif line.startsWith("camera "):
        namespace.permissions.camera = extractBool(line)
      elif line.startsWith("microphone "):
        namespace.permissions.microphone = extractBool(line)

    elif currentSection == "filesystem":
      if line.startsWith("access \""):
        let parts = line.split("\"")
        if parts.len >= 4:
          currentFsAccess = FilesystemAccess(path: parts[1], mode: ReadOnly)
          let modeStr = parts[3].toLowerAscii()
          case modeStr:
          of "readonly": currentFsAccess.mode = ReadOnly
          of "readwrite": currentFsAccess.mode = ReadWrite
          of "create": currentFsAccess.mode = Create
          else: currentFsAccess.mode = ReadOnly
          namespace.permissions.filesystem.add(currentFsAccess)

    elif currentSection == "dbus":
      if line.startsWith("session \""):
        let sessStr = extractQuoted(line)
        namespace.permissions.dbus.session = sessStr.split()
      elif line.startsWith("system \""):
        let sysStr = extractQuoted(line)
        namespace.permissions.dbus.system = sysStr.split()
      elif line.startsWith("own \""):
        let ownStr = extractQuoted(line)
        namespace.permissions.dbus.own = ownStr.split()

    elif currentSection == "mounts":
      if line.startsWith("mount {"):
        currentMount = Mount(source: "", target: "", mountType: Bind, readOnly: false)
      elif line.startsWith("source \""):
        currentMount.source = extractQuoted(line)
      elif line.startsWith("target \""):
        currentMount.target = extractQuoted(line)
      elif line.startsWith("type \""):
        let typeStr = extractQuoted(line)
        case typeStr:
        of "bind": currentMount.mountType = Bind
        of "tmpfs": currentMount.mountType = Tmpfs
        of "devtmpfs": currentMount.mountType = Devtmpfs
        else: currentMount.mountType = Bind
      elif line.startsWith("read_only "):
        currentMount.readOnly = extractBool(line)
      elif line == "}":
        if currentMount.source.len > 0:
          namespace.mounts.add(currentMount)
          currentMount = Mount(source: "", target: "", mountType: Bind, readOnly: false)
          skipSectionReset = true  # Don't reset section, we're still in mounts

    elif currentSection == "signature":
      if line.startsWith("algorithm \""):
        signature.algorithm = extractQuoted(line)
      elif line.startsWith("key_id \""):
        signature.keyId = extractQuoted(line)
      elif line.startsWith("signature \""):
        signature.signature = extractQuoted(line)

    # Reset section on closing brace (unless we just processed a nested block)
    if line == "}" and currentSection != "" and not skipSectionReset:
      if currentSection in ["metadata", "provenance", "build_config", "desktop", "namespace", "signature"]:
        currentSection = ""
      elif currentSection == "desktop_file":
        currentSection = "desktop"
      elif currentSection == "icons":
        currentSection = "desktop"
      elif currentSection == "permissions":
        currentSection = "namespace"
      elif currentSection == "filesystem":
        currentSection = "permissions"
      elif currentSection == "dbus":
        currentSection = "permissions"
      elif currentSection == "mounts":
        currentSection = "namespace"
      elif currentSection == "cas_chunks":
        currentSection = ""

    # Reset the skip flag for next iteration
    skipSectionReset = false

    i += 1

  result = NIPManifest(
    name: name,
    version: version,
    buildDate: buildDate,
    metadata: metadata,
    provenance: provenance,
    buildConfig: buildConfig,
    casChunks: casChunks,
    desktop: desktop,
    namespace: namespace,
    buildHash: buildHash,
    signature: signature
  )

# ============================================================================
# KDL Generation
# ============================================================================

proc generateNIPManifest*(manifest: NIPManifest): string =
  ## Generate KDL manifest from NIPManifest
  ##
  ## **Requirements:**
  ## - Requirement 4.2: Generate app name, version, permissions, namespace config, CAS chunks
  ## - Requirement 4.3: Generate .desktop file, icons, MIME type associations
  ## - Requirement 6.4: Deterministic generation (same input = same output)
  ##
  ## **Determinism:** Fields are output in a fixed order to ensure same input = same output

  result = "app \"" & manifest.name & "\" {\n"

  # Core identity
  result.add("  version \"" & $manifest.version & "\"\n")
  result.add("  build_date \"" & manifest.buildDate.format("yyyy-MM-dd'T'HH:mm:ss'Z'") & "\"\n")
  result.add("\n")

  # Metadata section
  result.add("  metadata {\n")
  result.add("    description \"" & manifest.metadata.description & "\"\n")
  result.add("    license \"" & manifest.metadata.license & "\"\n")
  if manifest.metadata.homepage.isSome:
    result.add("    homepage \"" & manifest.metadata.homepage.get() & "\"\n")
  if manifest.metadata.author.isSome:
    result.add("    author \"" & manifest.metadata.author.get() & "\"\n")
  if manifest.metadata.maintainer.isSome:
    result.add("    maintainer \"" & manifest.metadata.maintainer.get() & "\"\n")
  if manifest.metadata.category.isSome:
    result.add("    category \"" & manifest.metadata.category.get() & "\"\n")
  if manifest.metadata.tags.len > 0:
    result.add("    tags \"" & manifest.metadata.tags.join(" ") & "\"\n")
  result.add("  }\n\n")

  # Provenance section
  result.add("  provenance {\n")
  result.add("    source \"" & manifest.provenance.source & "\"\n")
  result.add("    source_hash \"" & manifest.provenance.sourceHash & "\"\n")
  if manifest.provenance.upstream.isSome:
    result.add("    upstream \"" & manifest.provenance.upstream.get() & "\"\n")
  result.add("    build_timestamp \"" & manifest.provenance.buildTimestamp.format("yyyy-MM-dd'T'HH:mm:ss'Z'") & "\"\n")
  if manifest.provenance.builder.isSome:
    result.add("    builder \"" & manifest.provenance.builder.get() & "\"\n")
  result.add("  }\n\n")

  # Build configuration section
  result.add("  build_config {\n")
  if manifest.buildConfig.configureFlags.len > 0:
    result.add("    configure_flags \"" & manifest.buildConfig.configureFlags.join(" ") & "\"\n")
  if manifest.buildConfig.compilerFlags.len > 0:
    result.add("    compiler_flags \"" & manifest.buildConfig.compilerFlags.join(" ") & "\"\n")
  result.add("    compiler_version \"" & manifest.buildConfig.compilerVersion & "\"\n")
  result.add("    target_architecture \"" & manifest.buildConfig.targetArchitecture & "\"\n")
  result.add("    libc \"" & manifest.buildConfig.libc & "\"\n")
  result.add("    allocator \"" & manifest.buildConfig.allocator & "\"\n")
  result.add("    build_system \"" & manifest.buildConfig.buildSystem & "\"\n")
  result.add("  }\n\n")

  # CAS chunks section
  if manifest.casChunks.len > 0:
    result.add("  cas_chunks {\n")
    for chunk in manifest.casChunks:
      result.add("    chunk \"" & chunk.hash & "\" {\n")
      result.add("      size " & $chunk.size & "\n")
      result.add("      type \"" & ($chunk.chunkType).toLowerAscii() & "\"\n")
      result.add("      path \"" & chunk.path & "\"\n")
      result.add("    }\n")
    result.add("  }\n\n")

  # Desktop integration section
  result.add("  desktop {\n")
  result.add("    app_id \"" & manifest.desktop.appId & "\"\n\n")

  # Desktop file
  result.add("    desktop_file {\n")
  result.add("      name \"" & manifest.desktop.desktopFile.name & "\"\n")
  if manifest.desktop.desktopFile.genericName.isSome:
    result.add("      generic_name \"" & manifest.desktop.desktopFile.genericName.get() & "\"\n")
  if manifest.desktop.desktopFile.comment.isSome:
    result.add("      comment \"" & manifest.desktop.desktopFile.comment.get() & "\"\n")
  result.add("      exec \"" & manifest.desktop.desktopFile.exec & "\"\n")
  result.add("      icon \"" & manifest.desktop.desktopFile.icon & "\"\n")
  result.add("      terminal " & $manifest.desktop.desktopFile.terminal & "\n")
  if manifest.desktop.desktopFile.categories.len > 0:
    result.add("      categories \"" & manifest.desktop.desktopFile.categories.join(";") & "\"\n")
  if manifest.desktop.desktopFile.keywords.len > 0:
    result.add("      keywords \"" & manifest.desktop.desktopFile.keywords.join(";") & "\"\n")
  result.add("    }\n\n")

  # Icons
  if manifest.desktop.icons.len > 0:
    result.add("    icons {\n")
    for icon in manifest.desktop.icons:
      result.add("      icon {\n")
      result.add("        size " & $icon.size & "\n")
      result.add("        path \"" & icon.path & "\"\n")
      result.add("        format \"" & icon.format & "\"\n")
      result.add("      }\n")
    result.add("    }\n\n")

  # MIME types
  if manifest.desktop.mimeTypes.len > 0:
    result.add("    mime_types \"" & manifest.desktop.mimeTypes.join(";") & "\"\n")

  result.add("  }\n\n")

  # Namespace configuration section
  result.add("  namespace {\n")
  result.add("    type \"" & manifest.namespace.namespaceType & "\"\n\n")

  # Permissions
  result.add("    permissions {\n")
  result.add("      network " & $manifest.namespace.permissions.network & "\n")
  result.add("      gpu " & $manifest.namespace.permissions.gpu & "\n")
  result.add("      audio " & $manifest.namespace.permissions.audio & "\n")
  result.add("      camera " & $manifest.namespace.permissions.camera & "\n")
  result.add("      microphone " & $manifest.namespace.permissions.microphone & "\n")

  # Filesystem access
  if manifest.namespace.permissions.filesystem.len > 0:
    result.add("\n      filesystem {\n")
    for fs in manifest.namespace.permissions.filesystem:
      result.add("        access \"" & fs.path & "\" \"" & ($fs.mode).toLowerAscii() & "\"\n")
    result.add("      }\n")

  # D-Bus access
  if manifest.namespace.permissions.dbus.session.len > 0 or
     manifest.namespace.permissions.dbus.system.len > 0 or
     manifest.namespace.permissions.dbus.own.len > 0:
    result.add("\n      dbus {\n")
    if manifest.namespace.permissions.dbus.session.len > 0:
      result.add("        session \"" & manifest.namespace.permissions.dbus.session.join(" ") & "\"\n")
    if manifest.namespace.permissions.dbus.system.len > 0:
      result.add("        system \"" & manifest.namespace.permissions.dbus.system.join(" ") & "\"\n")
    if manifest.namespace.permissions.dbus.own.len > 0:
      result.add("        own \"" & manifest.namespace.permissions.dbus.own.join(" ") & "\"\n")
    result.add("      }\n")

  result.add("    }\n")

  # Mounts
  if manifest.namespace.mounts.len > 0:
    result.add("\n    mounts {\n")
    for mount in manifest.namespace.mounts:
      result.add("      mount {\n")
      result.add("        source \"" & mount.source & "\"\n")
      result.add("        target \"" & mount.target & "\"\n")
      result.add("        type \"" & ($mount.mountType).toLowerAscii() & "\"\n")
      result.add("        read_only " & $mount.readOnly & "\n")
      result.add("      }\n")
    result.add("    }\n")

  result.add("  }\n\n")

  # Build hash
  result.add("  build_hash \"" & manifest.buildHash & "\"\n\n")

  # Signature
  result.add("  signature {\n")
  result.add("    algorithm \"" & manifest.signature.algorithm & "\"\n")
  result.add("    key_id \"" & manifest.signature.keyId & "\"\n")
  result.add("    signature \"" & manifest.signature.signature & "\"\n")
  result.add("  }\n")

  result.add("}\n")

# ============================================================================
# Validation
# ============================================================================

proc validateNIPManifest*(manifest: NIPManifest): seq[string] =
  ## Validate NIP manifest and return list of issues
  ##
  ## **Requirements:**
  ## - Requirement 6.3: Validate all required fields and hash formats
  ## - Requirement 4.2: Validate permissions and namespace config

  result = @[]

  # Validate name
  if manifest.name.len == 0:
    result.add("Application name cannot be empty")

  # Validate build hash format (xxh3-128)
  if manifest.buildHash.len > 0 and not manifest.buildHash.startsWith("xxh3-"):
    result.add("Build hash must use xxh3-128 format (xxh3-...)")

  # Validate source hash format
  if manifest.provenance.sourceHash.len > 0 and not manifest.provenance.sourceHash.startsWith("xxh3-"):
    result.add("Source hash must use xxh3-128 format (xxh3-...)")

  # Validate CAS chunks have xxh3 hashes
  for chunk in manifest.casChunks:
    if not chunk.hash.startsWith("xxh3-"):
      result.add("Chunk hash must use xxh3-128 format (xxh3-...)")
    if chunk.size <= 0:
      result.add("Chunk size must be positive")

  # Validate desktop integration
  if manifest.desktop.appId.len == 0:
    result.add("Desktop app_id cannot be empty")
  if manifest.desktop.desktopFile.name.len == 0:
    result.add("Desktop file name cannot be empty")
  if manifest.desktop.desktopFile.exec.len == 0:
    result.add("Desktop file exec command cannot be empty")

  # Validate namespace type
  if manifest.namespace.namespaceType notin ["user", "strict", "none"]:
    result.add("Namespace type must be 'user', 'strict', or 'none'")

  # Validate signature
  if manifest.signature.algorithm.len > 0 and manifest.signature.algorithm != "ed25519":
    result.add("Signature algorithm must be 'ed25519'")
  if manifest.signature.keyId.len == 0:
    result.add("Signature key_id cannot be empty")
  if manifest.signature.signature.len == 0:
    result.add("Signature value cannot be empty")

# ============================================================================
# Convenience Functions
# ============================================================================

proc `$`*(manifest: NIPManifest): string =
  ## Convert NIP manifest to human-readable string
  result = "NIP Application: " & manifest.name & " v" & $manifest.version & "\n"
  result.add("Build Date: " & manifest.buildDate.format("yyyy-MM-dd HH:mm:ss") & "\n")
  result.add("License: " & manifest.metadata.license & "\n")
  result.add("App ID: " & manifest.desktop.appId & "\n")
  result.add("Build Hash: " & manifest.buildHash & "\n")
  result.add("CAS Chunks: " & $manifest.casChunks.len & "\n")
  result.add("Namespace: " & manifest.namespace.namespaceType & "\n")
