## NPK Archive Handler
##
## **Purpose:**
## Handles .npk (Nexus Package Kit) archive creation and parsing.
## NPK packages are tar.zst archives containing manifest.kdl, metadata.json,
## CAS chunks, and Ed25519 signatures.
##
## **Design Principles:**
## - System packages installed to /Programs/App/Version/
## - Content-addressable storage for deduplication
## - Atomic operations with rollback capability
## - Ed25519 signature verification
##
## **Requirements:**
## - Requirement 3.1: .npk contains manifest.kdl, metadata.json, CAS chunks, Ed25519 signature
## - Requirement 8.2: Use zstd --auto for archive compression
##
## **Archive Structure:**
## ```
## package.npk (tar.zst)
## ├── manifest.kdl          # Package metadata
## ├── metadata.json         # Additional metadata
## ├── chunks/               # CAS chunks
## │   ├── xxh3-abc123.zst
## │   ├── xxh3-def456.zst
## │   └── ...
## └── signature.sig         # Ed25519 signature
## ```

import std/[os, strutils, times, json, options, sequtils]
import nip/cas
import nip/xxhash
import nip/npk_manifest
import nip/manifest_parser

type
  NPKPackage* = object
    ## Complete NPK package with all components
    manifest*: NPKManifest
    metadata*: JsonNode
    chunks*: seq[ChunkData]
    signature*: string
    archivePath*: string

  ChunkData* = object
    ## Chunk data extracted from archive
    hash*: string
    data*: string
    size*: int64
    chunkType*: ChunkType

  NPKError* = object of CatchableError
    code*: NPKErrorCode
    context*: string
    suggestions*: seq[string]

  NPKErrorCode* = enum
    ArchiveNotFound,
    InvalidArchive,
    ManifestMissing,
    SignatureMissing,
    ChunkMissing,
    ExtractionFailed,
    CompressionFailed,
    InvalidFormat

# ============================================================================
# Archive Parsing
# ============================================================================

proc parseNPK*(path: string): NPKPackage =
  ## Parse .npk archive and extract all components
  ##
  ## **Requirements:**
  ## - Requirement 3.1: Extract manifest.kdl, metadata.json, CAS chunks, signature
  ## - Requirement 8.2: Handle zstd --auto compressed archives
  ##
  ## **Process:**
  ## 1. Verify archive exists and is readable
  ## 2. Extract to temporary directory
  ## 3. Parse manifest.kdl
  ## 4. Parse metadata.json
  ## 5. Load chunks from chunks/ directory
  ## 6. Load signature from signature.sig
  ## 7. Verify integrity
  ##
  ## **Raises:**
  ## - NPKError if archive is invalid or missing components

  if not fileExists(path):
    raise newException(NPKError, "NPK archive not found: " & path)

  # Create temporary extraction directory
  let tempDir = getTempDir() / "npk-extract-" & $getTime().toUnix()
  createDir(tempDir)

  try:
    # Extract archive using tar with zstd decompression
    # Using --auto-compress lets tar detect compression automatically
    let extractCmd = "tar --auto-compress -xf " & quoteShell(path) & " -C " & quoteShell(tempDir)
    let extractResult = execShellCmd(extractCmd)

    if extractResult != 0:
      raise newException(NPKError, "Failed to extract NPK archive: " & path)

    # Parse manifest.kdl
    let manifestPath = tempDir / "manifest.kdl"
    if not fileExists(manifestPath):
      raise newException(NPKError, "manifest.kdl not found in archive")

    let manifestKdl = readFile(manifestPath)
    let manifest = parseNPKManifest(manifestKdl)

    # Parse metadata.json
    let metadataPath = tempDir / "metadata.json"
    var metadata = newJObject()
    if fileExists(metadataPath):
      let metadataStr = readFile(metadataPath)
      metadata = parseJson(metadataStr)

    # Load chunks from chunks/ directory
    var chunks: seq[ChunkData] = @[]
    let chunksDir = tempDir / "chunks"
    if dirExists(chunksDir):
      for chunkFile in walkFiles(chunksDir / "*.zst"):
        let chunkName = extractFilename(chunkFile)
        let chunkHash = chunkName.replace(".zst", "")

        # Read compressed chunk data
        let chunkData = readFile(chunkFile)

        chunks.add(ChunkData(
          hash: chunkHash,
          data: chunkData,
          size: chunkData.len.int64,
          chunkType: Binary  # Will be determined from manifest
        ))

    # Load signature
    let signaturePath = tempDir / "signature.sig"
    var signature = ""
    if fileExists(signaturePath):
      signature = readFile(signaturePath)

    result = NPKPackage(
      manifest: manifest,
      metadata: metadata,
      chunks: chunks,
      signature: signature,
      archivePath: path
    )

  finally:
    # Clean up temporary directory
    if dirExists(tempDir):
      removeDir(tempDir)

# ============================================================================
# Archive Creation
# ============================================================================

proc createNPK*(manifest: NPKManifest, chunks: seq[ChunkData],
                metadata: JsonNode, signature: string,
                outputPath: string): NPKPackage =
  ## Create .npk archive from components
  ##
  ## **Requirements:**
  ## - Requirement 3.1: Package manifest.kdl, metadata.json, CAS chunks, signature
  ## - Requirement 8.2: Use zstd --auto for archive compression
  ##
  ## **Process:**
  ## 1. Create temporary staging directory
  ## 2. Write manifest.kdl
  ## 3. Write metadata.json
  ## 4. Write chunks to chunks/ directory
  ## 5. Write signature.sig
  ## 6. Create tar.zst archive with --auto-compress
  ## 7. Verify archive integrity
  ##
  ## **Returns:**
  ## - NPKPackage with all components
  ##
  ## **Raises:**
  ## - NPKError if creation fails

  # Create temporary staging directory
  let tempDir = getTempDir() / "npk-create-" & $getTime().toUnix()
  createDir(tempDir)

  try:
    # Write manifest.kdl
    let manifestKdl = generateNPKManifest(manifest)
    writeFile(tempDir / "manifest.kdl", manifestKdl)

    # Write metadata.json
    writeFile(tempDir / "metadata.json", $metadata)

    # Write chunks to chunks/ directory
    let chunksDir = tempDir / "chunks"
    createDir(chunksDir)

    for chunk in chunks:
      let chunkPath = chunksDir / (chunk.hash & ".zst")
      writeFile(chunkPath, chunk.data)

    # Write signature
    writeFile(tempDir / "signature.sig", signature)

    # Create tar.zst archive
    # Using --auto-compress lets tar choose optimal compression
    let createCmd = "tar --auto-compress -cf " & quoteShell(outputPath) &
                    " -C " & quoteShell(tempDir) & " ."
    let createResult = execShellCmd(createCmd)

    if createResult != 0:
      raise newException(NPKError, "Failed to create NPK archive: " & outputPath)

    result = NPKPackage(
      manifest: manifest,
      metadata: metadata,
      chunks: chunks,
      signature: signature,
      archivePath: outputPath
    )

  finally:
    # Clean up temporary directory
    if dirExists(tempDir):
      removeDir(tempDir)

# ============================================================================
# Chunk Extraction
# ============================================================================

proc extractChunks*(pkg: NPKPackage, casRoot: string): seq[string] =
  ## Extract chunks from NPK package to CAS
  ##
  ## **Requirements:**
  ## - Requirement 3.1: Extract CAS chunks from archive
  ## - Requirement 2.1: Store chunks with xxh3-128 hashing
  ##
  ## **Process:**
  ## 1. For each chunk in package
  ## 2. Decompress chunk data (if compressed)
  ## 3. Calculate xxh3-128 hash
  ## 4. Verify hash matches manifest
  ## 5. Store in CAS with deduplication
  ## 6. Return list of stored chunk hashes
  ##
  ## **Returns:**
  ## - List of chunk hashes stored in CAS
  ##
  ## **Raises:**
  ## - NPKError if chunk extraction or verification fails

  result = @[]

  for chunk in pkg.chunks:
    # Decompress chunk data
    # TODO: Implement zstd decompression when library available
    let decompressedData = chunk.data

    # Calculate xxh3-128 hash
    let calculatedHash = $calculateXxh3(decompressedData)

    # Verify hash matches manifest
    let manifestChunk = pkg.manifest.casChunks.filterIt(it.hash == chunk.hash)
    if manifestChunk.len == 0:
      raise newException(NPKError, "Chunk not found in manifest: " & chunk.hash)

    if calculatedHash != chunk.hash:
      raise newException(NPKError,
        "Chunk hash mismatch: expected " & chunk.hash & ", got " & calculatedHash)

    # Store in CAS (will deduplicate automatically)
    let casObject = storeObject(decompressedData, casRoot, compress = true)

    result.add(string(casObject.hash))

# ============================================================================
# Verification
# ============================================================================

proc verifyNPK*(pkg: NPKPackage): bool =
  ## Verify NPK package integrity
  ##
  ## **Requirements:**
  ## - Requirement 3.4: Verify Ed25519 signature
  ## - Requirement 2.2: Verify chunk integrity using xxh3 hash
  ##
  ## **Checks:**
  ## 1. Manifest is valid
  ## 2. All chunks referenced in manifest are present
  ## 3. Chunk hashes match manifest
  ## 4. Signature is valid (if present)
  ##
  ## **Returns:**
  ## - true if package is valid, false otherwise

  # Validate manifest
  let issues = validateNPKManifest(pkg.manifest)
  if issues.len > 0:
    return false

  # Verify all chunks are present
  for manifestChunk in pkg.manifest.casChunks:
    let found = pkg.chunks.anyIt(it.hash == manifestChunk.hash)
    if not found:
      return false

  # Verify chunk hashes
  for chunk in pkg.chunks:
    # TODO: Implement hash verification when xxh3 library available
    discard

  # Verify signature
  # TODO: Implement Ed25519 signature verification
  if pkg.signature.len == 0:
    return false

  result = true

# ============================================================================
# Utility Functions
# ============================================================================

proc listChunks*(pkg: NPKPackage): seq[string] =
  ## List all chunk hashes in package
  result = pkg.chunks.mapIt(it.hash)

proc getChunk*(pkg: NPKPackage, hash: string): Option[ChunkData] =
  ## Get chunk data by hash
  for chunk in pkg.chunks:
    if chunk.hash == hash:
      return some(chunk)
  return none(ChunkData)

proc packageSize*(pkg: NPKPackage): int64 =
  ## Calculate total package size (sum of all chunks)
  result = 0
  for chunk in pkg.chunks:
    result += chunk.size

proc `$`*(pkg: NPKPackage): string =
  ## Convert NPK package to human-readable string
  result = "NPK Package: " & pkg.manifest.name & " v" & manifest_parser.`$`(pkg.manifest.version) & "\n"
  result.add("Archive: " & pkg.archivePath & "\n")
  result.add("Chunks: " & $pkg.chunks.len & "\n")
  result.add("Total Size: " & $(packageSize(pkg) div 1024) & " KB\n")
  result.add("Signature: " & (if pkg.signature.len > 0: "Present" else: "Missing") & "\n")

# ============================================================================
# Error Formatting
# ============================================================================

proc formatNPKError*(error: NPKError): string =
  ## Format NPK error with context and suggestions
  result = "❌ [" & $error.code & "] " & error.msg & "\n"
  if error.context.len > 0:
    result.add("🔍 Context: " & error.context & "\n")
  if error.suggestions.len > 0:
    result.add("💡 Suggestions:\n")
    for suggestion in error.suggestions:
      result.add("  • " & suggestion & "\n")
