## NPK Manifest Schema - System Package Format
##
## **Purpose:**
## Defines the NPK (Nexus Package Kit) manifest schema for system packages.
## NPK packages are installed system-wide and managed by nexus.
##
## **Design Principles:**
## - Complete metadata for system packages
## - Build configuration tracking for reproducibility
## - Dependency resolution with build hashes
## - System integration (services, users, paths)
## - Ed25519 signature support
##
## **Requirements:**
## - Requirement 3.1: manifest.kdl, metadata.json, CAS chunks, Ed25519 signature
## - Requirement 3.2: package name, version, dependencies, build config, CAS chunk references
## - 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]
import nip/manifest_parser
import nimpak/kdl_parser

type
  # ============================================================================
  # NPK-Specific Types
  # ============================================================================

  NPKManifest* = object
    ## Complete NPK manifest for system packages
    # Core identity (from base PackageManifest)
    name*: string
    version*: SemanticVersion
    buildDate*: DateTime

    # Package metadata
    metadata*: PackageInfo
    provenance*: ProvenanceInfo
    buildConfig*: BuildConfiguration

    # Dependencies with build hashes
    dependencies*: seq[Dependency]

    # CAS chunk references
    casChunks*: seq[ChunkReference]

    # Installation paths (GoboLinux-style)
    install*: InstallPaths

    # System integration
    system*: SystemIntegration

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

  PackageInfo* = object
    ## Package metadata
    description*: string
    homepage*: Option[string]
    license*: string
    author*: Option[string]
    maintainer*: Option[string]
    tags*: seq[string]

  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.

  Dependency* = object
    ## Package dependency with build hash
    name*: string
    version*: string
    buildHash*: string       ## xxh3-128 hash of dependency's build config
    optional*: bool

  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

  InstallPaths* = object
    ## GoboLinux-style installation paths
    programsPath*: string    ## /Programs/App/Version/
    binPath*: string         ## /Programs/App/Version/bin/
    libPath*: string         ## /Programs/App/Version/lib/
    sharePath*: string       ## /Programs/App/Version/share/
    etcPath*: string         ## /Programs/App/Version/etc/

  SystemIntegration* = object
    ## System-level integration
    services*: seq[ServiceSpec]
    users*: seq[UserSpec]
    groups*: seq[GroupSpec]
    systemPaths*: seq[string]  ## Paths to link into /System/Index/

  ServiceSpec* = object
    ## System service specification
    name*: string
    serviceType*: string     ## systemd, dinit, etc.
    enable*: bool
    startOnBoot*: bool

  UserSpec* = object
    ## System user specification
    name*: string
    uid*: Option[int]
    system*: bool
    shell*: string
    home*: Option[string]

  GroupSpec* = object
    ## System group specification
    name*: string
    gid*: Option[int]
    system*: bool

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

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

  NPKError* = object of CatchableError
    code*: NPKErrorCode
    context*: string

  NPKErrorCode* = enum
    InvalidManifest,
    MissingField,
    InvalidHash,
    InvalidSignature,
    DependencyError

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

proc parseNPKManifest*(kdl: string): NPKManifest =
  ## Parse NPK manifest from KDL format
  ##
  ## **Requirements:**
  ## - Requirement 3.2: Parse package name, version, dependencies, build config, CAS chunks
  ## - 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 = PackageInfo(description: "", license: "", tags: @[])
  var provenance = ProvenanceInfo(source: "", sourceHash: "", buildTimestamp: now())
  var buildConfig = BuildConfiguration(
    configureFlags: @[], compilerFlags: @[],
    compilerVersion: "", targetArchitecture: "",
    libc: "", allocator: "", buildSystem: ""
  )
  var dependencies: seq[Dependency] = @[]
  var casChunks: seq[ChunkReference] = @[]
  var install = InstallPaths(
    programsPath: "", binPath: "", libPath: "", sharePath: "", etcPath: ""
  )
  var system = SystemIntegration(
    services: @[], users: @[], groups: @[], systemPaths: @[]
  )
  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 currentDep: Dependency
  var currentChunk: ChunkReference
  var currentService: ServiceSpec
  var currentUser: UserSpec
  var currentGroup: GroupSpec
  var skipSectionReset = false  # Flag to skip section reset for nested blocks

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

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

    # Parse top-level fields
    elif line.startsWith("version \"") and currentSection == "":
      let vstr = extractQuoted(line)
      # Parse semantic version (e.g., "1.2.3-alpha.1+build.2")
      # Split by "." but handle prerelease and build metadata
      let dashIdx = vstr.find("-")
      let plusIdx = vstr.find("+")

      # Extract core version (before - or +)
      var coreVersion = vstr
      if dashIdx >= 0:
        coreVersion = vstr[0..<dashIdx]
      elif plusIdx >= 0:
        coreVersion = vstr[0..<plusIdx]

      let parts = coreVersion.split(".")
      if parts.len >= 3:
        version = SemanticVersion(
          major: parseInt(parts[0]),
          minor: parseInt(parts[1]),
          patch: parseInt(parts[2]),
          prerelease: "",
          build: ""
        )

        # Parse prerelease if present
        if dashIdx >= 0:
          let endIdx = if plusIdx >= 0: plusIdx else: vstr.len
          version.prerelease = vstr[dashIdx+1..<endIdx]

        # Parse build metadata if present
        if plusIdx >= 0:
          version.build = vstr[plusIdx+1..^1]

    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 == "dependencies {":
      currentSection = "dependencies"
    elif line == "cas_chunks {":
      currentSection = "cas_chunks"
    elif line == "install {":
      currentSection = "install"
    elif line == "system {":
      currentSection = "system"
    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("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 == "dependencies":
      if line.startsWith("dependency \""):
        currentDep = Dependency(name: extractQuoted(line), version: "", buildHash: "", optional: false)
      elif line.startsWith("version \"") and currentDep.name.len > 0:
        currentDep.version = extractQuoted(line)
      elif line.startsWith("build_hash \"") and currentDep.name.len > 0:
        currentDep.buildHash = extractQuoted(line)
      elif line.startsWith("optional ") and currentDep.name.len > 0:
        currentDep.optional = extractBool(line)
      elif line == "}":
        if currentDep.name.len > 0:
          dependencies.add(currentDep)
          currentDep = Dependency(name: "", version: "", buildHash: "", optional: false)
          skipSectionReset = true  # Don't reset section, we're still in dependencies

    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 == "install":
      if line.startsWith("programs_path \""):
        install.programsPath = extractQuoted(line)
      elif line.startsWith("bin_path \""):
        install.binPath = extractQuoted(line)
      elif line.startsWith("lib_path \""):
        install.libPath = extractQuoted(line)
      elif line.startsWith("share_path \""):
        install.sharePath = extractQuoted(line)
      elif line.startsWith("etc_path \""):
        install.etcPath = extractQuoted(line)

    elif currentSection == "system":
      if line.startsWith("service \""):
        currentService = ServiceSpec(name: extractQuoted(line), serviceType: "", enable: false, startOnBoot: false)
      elif line.startsWith("type \""):
        currentService.serviceType = extractQuoted(line)
      elif line.startsWith("enable "):
        currentService.enable = extractBool(line)
      elif line.startsWith("start_on_boot "):
        currentService.startOnBoot = extractBool(line)
      elif line.startsWith("user \""):
        currentUser = UserSpec(name: extractQuoted(line), uid: none(int), system: false, shell: "", home: none(string))
      elif line.startsWith("uid "):
        currentUser.uid = some(extractInt(line))
      elif line.startsWith("system "):
        if currentUser.name.len > 0:
          currentUser.system = extractBool(line)
        elif currentGroup.name.len > 0:
          currentGroup.system = extractBool(line)
      elif line.startsWith("shell \""):
        currentUser.shell = extractQuoted(line)
      elif line.startsWith("home \""):
        currentUser.home = some(extractQuoted(line))
      elif line.startsWith("group \""):
        currentGroup = GroupSpec(name: extractQuoted(line), gid: none(int), system: false)
      elif line.startsWith("gid "):
        currentGroup.gid = some(extractInt(line))
      elif line == "}":
        if currentService.name.len > 0:
          system.services.add(currentService)
          currentService = ServiceSpec(name: "", serviceType: "", enable: false, startOnBoot: false)
          skipSectionReset = true
        elif currentUser.name.len > 0:
          system.users.add(currentUser)
          currentUser = UserSpec(name: "", uid: none(int), system: false, shell: "", home: none(string))
          skipSectionReset = true
        elif currentGroup.name.len > 0:
          system.groups.add(currentGroup)
          currentGroup = GroupSpec(name: "", gid: none(int), system: false)
          skipSectionReset = true

    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", "dependencies", "cas_chunks", "install", "system", "signature"]:
        currentSection = ""

    # Reset the skip flag for next iteration
    skipSectionReset = false

    i += 1

  result = NPKManifest(
    name: name,
    version: version,
    buildDate: buildDate,
    metadata: metadata,
    provenance: provenance,
    buildConfig: buildConfig,
    dependencies: dependencies,
    casChunks: casChunks,
    install: install,
    system: system,
    buildHash: buildHash,
    signature: signature
  )

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

proc generateNPKManifest*(manifest: NPKManifest): string =
  ## Generate KDL manifest from NPKManifest
  ##
  ## **Requirements:**
  ## - Requirement 3.2: Generate package name, version, dependencies, build config, CAS chunks
  ## - Requirement 6.4: Deterministic generation (same input = same output)
  ##
  ## **Determinism:** Fields are output in a fixed order to ensure same input = same output

  result = "package \"" & 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.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")

  # Dependencies section
  if manifest.dependencies.len > 0:
    result.add("  dependencies {\n")
    for dep in manifest.dependencies:
      result.add("    dependency \"" & dep.name & "\" {\n")
      result.add("      version \"" & dep.version & "\"\n")
      result.add("      build_hash \"" & dep.buildHash & "\"\n")
      if dep.optional:
        result.add("      optional true\n")
      result.add("    }\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")

  # Install paths section
  result.add("  install {\n")
  result.add("    programs_path \"" & manifest.install.programsPath & "\"\n")
  result.add("    bin_path \"" & manifest.install.binPath & "\"\n")
  result.add("    lib_path \"" & manifest.install.libPath & "\"\n")
  result.add("    share_path \"" & manifest.install.sharePath & "\"\n")
  result.add("    etc_path \"" & manifest.install.etcPath & "\"\n")
  result.add("  }\n\n")

  # System integration section
  if manifest.system.services.len > 0 or manifest.system.users.len > 0 or manifest.system.groups.len > 0:
    result.add("  system {\n")

    # Services
    for service in manifest.system.services:
      result.add("    service \"" & service.name & "\" {\n")
      result.add("      type \"" & service.serviceType & "\"\n")
      result.add("      enable " & $service.enable & "\n")
      result.add("      start_on_boot " & $service.startOnBoot & "\n")
      result.add("    }\n")

    # Users
    for user in manifest.system.users:
      result.add("    user \"" & user.name & "\" {\n")
      if user.uid.isSome:
        result.add("      uid " & $user.uid.get() & "\n")
      result.add("      system " & $user.system & "\n")
      result.add("      shell \"" & user.shell & "\"\n")
      if user.home.isSome:
        result.add("      home \"" & user.home.get() & "\"\n")
      result.add("    }\n")

    # Groups
    for group in manifest.system.groups:
      result.add("    group \"" & group.name & "\" {\n")
      if group.gid.isSome:
        result.add("      gid " & $group.gid.get() & "\n")
      result.add("      system " & $group.system & "\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 validateNPKManifest*(manifest: NPKManifest): seq[string] =
  ## Validate NPK manifest and return list of issues
  ##
  ## **Requirements:**
  ## - Requirement 6.3: Validate all required fields and hash formats
  ## - Requirement 6.5: Validate exact versions and build hashes for dependencies

  result = @[]

  # Validate name
  if manifest.name.len == 0:
    result.add("Package 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 dependencies have build hashes
  for dep in manifest.dependencies:
    if dep.buildHash.len == 0:
      result.add("Dependency '" & dep.name & "' missing build hash")
    elif not dep.buildHash.startsWith("xxh3-"):
      result.add("Dependency '" & dep.name & "' build hash must use xxh3-128 format")

  # 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 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: NPKManifest): string =
  ## Convert NPK manifest to human-readable string
  result = "NPK Package: " & 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("Build Hash: " & manifest.buildHash & "\n")
  result.add("Dependencies: " & $manifest.dependencies.len & "\n")
  result.add("CAS Chunks: " & $manifest.casChunks.len & "\n")
