## Publish & Push Pipeline for NexusForge
##
## This module provides:
## - Artifact building from fragments/sources
## - Package signing with Ed25519
## - Repository upload (native + foreign)
##
## Leverages existing infrastructure:
## - `packages.nim` for NPK creation
## - `signature.nim` for Ed25519 signing
## - `cas.nim` for content-addressable storage
## - `remote/manager.nim` for repository access

import std/[os, strformat, strutils, json, options, times, sequtils, httpclient, asyncdispatch]
import ../cas
import ../types_fixed
import ../formats
import ../signature
import ../packages
import ../security/signature_verifier
import ../security/provenance_tracker
import ../remote/manager

type
  PublishConfig* = object
    ## Configuration for publishing packages
    keyId*: string               # Signing key ID
    repoId*: string              # Target repository ID
    dryRun*: bool                # Don't actually upload
    signPackage*: bool           # Sign with Ed25519
    includeProvenance*: bool     # Include provenance chain
    compressionLevel*: int       # zstd compression level (1-19)
    
  PublishResult* = object
    success*: bool
    packageName*: string
    version*: string
    casHash*: string             # CAS hash of published package
    signature*: string           # Ed25519 signature
    repoUrl*: string             # URL in repository
    uploadedBytes*: int64
    errors*: seq[string]

  ArtifactSourceKind* = enum
    FromDirectory,
    FromCas,
    FromGraft

  ArtifactSource* = object
    ## Source for building an artifact
    case kind*: ArtifactSourceKind
    of FromDirectory:
      sourceDir*: string
    of FromCas:
      files*: seq[types_fixed.PackageFile]
    of FromGraft:
      graftResult*: types_fixed.GraftResult

  ArtifactBuilder* = ref object
    cas*: CasManager
    sigManager*: SignatureManager
    config*: PublishConfig
    outputDir*: string

# =============================================================================
# Artifact Building
# =============================================================================

proc newArtifactBuilder*(casRoot: string, keysRoot: string, 
                        outputDir: string = ""): ArtifactBuilder =
  ## Create a new artifact builder
  let actualOutput = if outputDir.len > 0: outputDir 
                     else: getTempDir() / "nip-publish"
  createDir(actualOutput)
  
  ArtifactBuilder(
    cas: initCasManager(casRoot),
    sigManager: initSignatureManager(keysRoot),
    config: PublishConfig(
      signPackage: true,
      includeProvenance: true,
      compressionLevel: 3,  # Fast compression
      dryRun: false
    ),
    outputDir: actualOutput
  )

proc buildFromDirectory*(builder: ArtifactBuilder, 
                        sourceDir: string,
                        name: string, 
                        version: string): types_fixed.Result[NpkPackage, string] =
  ## Build NPK package from a directory of files
  try:
    # Collect all files
    var files: seq[PackageFile] = @[]
    var totalSize: int64 = 0
    
    for file in walkDirRec(sourceDir, relative = true):
      let fullPath = sourceDir / file
      if fileExists(fullPath):
        let data = readFile(fullPath)
        let dataBytes = data.toOpenArrayByte(0, data.len - 1).toSeq()
        
        # Store in CAS and get hash
        let storeResult = builder.cas.storeObject(dataBytes)
        if cas.isErr(storeResult):
          return types_fixed.err[NpkPackage, string]("Failed to store file " & file & " in CAS")
        
        let casObj = cas.get(storeResult)
        let info = getFileInfo(fullPath)
        
        files.add(PackageFile(
          path: file,
          hash: casObj.hash,
          hashAlgorithm: "blake2b",
          permissions: FilePermissions(
            mode: cast[int](info.permissions),
            owner: "root",
            group: "root"
          ),
          chunks: none(seq[types_fixed.ChunkRef])
        ))
        totalSize += casObj.size
    
    if files.len == 0:
      return types_fixed.err[NpkPackage, string]("No files found in source directory")
    
    # Calculate merkle root from file hashes
    var allHashes = files.mapIt(it.hash).join("")
    let merkleRoot = cas.calculateBlake2b(
      allHashes.toOpenArrayByte(0, allHashes.len - 1).toSeq()
    )
    
    # Create package manifest
    let manifest = PackageManifest(
      files: files,
      totalSize: totalSize,
      created: now(),
      merkleRoot: merkleRoot
    )
    
    # Create NPK package
    var npk = NpkPackage(
      metadata: Fragment(
        id: PackageId(
          name: name,
          version: version,
          stream: Stable
        )
      ),
      files: files,
      manifest: manifest,
      format: NpkBinary,
      cryptoAlgorithms: getDefaultCryptoAlgorithms(NpkBinary)
    )
    
    return types_fixed.ok[NpkPackage, string](npk)
    
  except Exception as e:
    return types_fixed.err[NpkPackage, string]("Exception building package: " & e.msg)

proc signPackage*(builder: ArtifactBuilder, npk: var NpkPackage): types_fixed.Result[string, string] =
  ## Sign the package with Ed25519
  if builder.config.keyId.len == 0:
    return types_fixed.err[string, string]("No signing key configured")
  
  try:
    # Create signature payload from package metadata
    let payload = npk.metadata.id.name & 
                  npk.metadata.id.version &
                  npk.manifest.merkleRoot &
                  $npk.manifest.totalSize &
                  $npk.manifest.created
    
    # Sign with Ed25519
    let signature = builder.sigManager.sign(payload, builder.config.keyId)
    
    # Add signature to package
    npk.signature = some(Signature(
      keyId: builder.config.keyId,
      algorithm: "Ed25519",
      signature: signature.toOpenArrayByte(0, signature.len - 1).toSeq()
    ))
    
    return types_fixed.ok[string, string](signature)
    
  except SignatureError as e:
    return types_fixed.err[string, string]("Signing failed: " & e.msg)
  except Exception as e:
    return types_fixed.err[string, string]("Exception signing package: " & e.msg)

proc createArchive*(builder: ArtifactBuilder, npk: NpkPackage): types_fixed.Result[string, string] =
  ## Create the .npk.zst archive file
  try:
    let archiveName = fmt"{npk.metadata.id.name}-{npk.metadata.id.version}.npk.zst"
    let archivePath = builder.outputDir / archiveName
    
    let createResult = createNpkArchive(npk, archivePath, NpkZst)
    if types_fixed.isErr(createResult):
      return types_fixed.err[string, string]("Failed to create archive: " & types_fixed.getError(createResult).msg)
    
    return types_fixed.ok[string, string](archivePath)
    
  except Exception as e:
    return types_fixed.err[string, string]("Exception creating archive: " & e.msg)

# =============================================================================
# Repository Upload
# =============================================================================

proc uploadToRepository*(builder: ArtifactBuilder, 
                        archivePath: string,
                        remoteManager: RemoteManager): Future[PublishResult] {.async.} =
  ## Upload package to configured repository
  var result = PublishResult(
    success: false,
    packageName: archivePath.extractFilename().split("-")[0]
  )
  
  if builder.config.dryRun:
    result.success = true
    result.repoUrl = "[dry-run]"
    return result
  
  try:
    # Get active repository
    let repoOpt = remoteManager.getRepository(builder.config.repoId)
    if repoOpt.isNone:
      result.errors.add("Repository not found: " & builder.config.repoId)
      return result
    
    let repo = repoOpt.get()
    
    # Read package data
    let packageData = readFile(archivePath)
    result.uploadedBytes = packageData.len.int64
    
    # Upload to repository
    let uploadUrl = repo.url / "api/v1/packages/upload"
    let uploadResult = remoteManager.makeSecureRequest(
      repo, 
      "api/v1/packages/upload",
      HttpPost,
      packageData
    )
    
    if not uploadResult.success:
      result.errors.add("Upload failed: " & uploadResult.error)
      return result
    
    # Parse response for URL
    let response = parseJson(uploadResult.value)
    result.repoUrl = response["url"].getStr("")
    result.casHash = response["hash"].getStr("")
    result.success = true
    
  except Exception as e:
    result.errors.add("Exception uploading: " & e.msg)
  
  return result

# =============================================================================
# High-Level Publish API
# =============================================================================

proc publish*(builder: ArtifactBuilder,
             source: ArtifactSource,
             name: string,
             version: string): Future[PublishResult] {.async.} =
  ## Full publish pipeline: build, sign, archive, upload
  var result = PublishResult(
    success: false,
    packageName: name,
    version: version
  )
  
  # Step 1: Build package from source
  var npkResult: types_fixed.Result[NpkPackage, string]
  
  case source.kind:
  of FromDirectory:
    npkResult = builder.buildFromDirectory(source.sourceDir, name, version)
  of FromCas:
    # Validate all files exist in CAS
    var files = source.files
    var totalSize: int64 = 0
    var valid = true
    
    for file in files:
      if not builder.cas.objectExists(file.hash):
        result.errors.add("CAS object missing for file: " & file.path & " (" & file.hash & ")")
        valid = false
      else:
        # Get size from CAS to ensure accuracy
        # Note: We'd need retrieveObject or similar to get stats without fetching
        # For now, we assume the caller provided valid info or we check basic existence
        discard
    
    if not valid:
      return result

    # Calculate merkle root
    var allHashes = files.mapIt(it.hash).join("")
    let merkleRoot = cas.calculateBlake2b(
      allHashes.toOpenArrayByte(0, allHashes.len - 1).toSeq()
    )
    
    let manifest = PackageManifest(
      files: files,
      totalSize: 0, # TODO: Calculate total size from CAS objects
      created: now(),
      merkleRoot: merkleRoot
    )
    
    var npk = NpkPackage(
      metadata: Fragment(
        id: PackageId(name: name, version: version, stream: Stable)
      ),
      files: files,
      manifest: manifest,
      format: NpkBinary,
      cryptoAlgorithms: getDefaultCryptoAlgorithms(NpkBinary)
    )
    npkResult = types_fixed.ok[NpkPackage, string](npk)

  of FromGraft:
    let convertResult = packages.convertGraftToNpk(source.graftResult, builder.cas)
    if types_fixed.isErr(convertResult):
      npkResult = types_fixed.err[NpkPackage, string]("Graft conversion failed: " & types_fixed.getError(convertResult).msg)
    else:
      npkResult = types_fixed.ok[NpkPackage, string](types_fixed.get(convertResult))
  
  if types_fixed.isErr(npkResult):
    result.errors.add(types_fixed.getError(npkResult))
    return result
  
  var npk = types_fixed.get(npkResult)
  
  # Step 2: Sign package (if enabled)
  if builder.config.signPackage and builder.config.keyId.len > 0:
    let signResult = builder.signPackage(npk)
    if types_fixed.isErr(signResult):
      result.errors.add(types_fixed.getError(signResult))
      # Continue without signature (warning)
    else:
      result.signature = types_fixed.get(signResult)
  
  # Step 3: Create archive
  let archiveResult = builder.createArchive(npk)
  if types_fixed.isErr(archiveResult):
    result.errors.add(types_fixed.getError(archiveResult))
    return result
  
  let archivePath = types_fixed.get(archiveResult)
  
  # Step 4: Store in CAS
  let archiveData = readFile(archivePath)
  let storeResult = builder.cas.storeObject(
    archiveData.toOpenArrayByte(0, archiveData.len - 1).toSeq()
  )
  
  if not cas.isErr(storeResult):
    result.casHash = cas.get(storeResult).hash
  
  # Step 5: Upload to repository (if configured)
  if builder.config.repoId.len > 0:
    let remoteConfig = getDefaultRemoteManagerConfig()
    let remoteManager = newRemoteManager(remoteConfig)
    
    let uploadResult = await builder.uploadToRepository(archivePath, remoteManager)
    if not uploadResult.success:
      result.errors.add(uploadResult.errors)
      # Continue - we still have the local package
    else:
      result.repoUrl = uploadResult.repoUrl
      result.uploadedBytes = uploadResult.uploadedBytes
  
  result.success = true
  return result

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

proc publishDirectory*(sourceDir: string, name: string, version: string,
                      keyId: string = "", repoId: string = "",
                      dryRun: bool = false): Future[PublishResult] {.async.} =
  ## Convenience function to publish a directory as a package
  let casRoot = expandTilde("~/.nip/cas")
  let keysRoot = expandTilde("~/.nip/keys")
  
  var builder = newArtifactBuilder(casRoot, keysRoot)
  builder.config.keyId = keyId
  builder.config.repoId = repoId
  builder.config.dryRun = dryRun
  
  let source = ArtifactSource(
    kind: FromDirectory,
    sourceDir: sourceDir
  )
  
  return await builder.publish(source, name, version)

proc getPublishInfo*(pubResult: PublishResult): string =
  ## Get human-readable publish result
  var info = fmt"📦 Package: {pubResult.packageName} v{pubResult.version}" & "\n"
  
  if pubResult.success:
    info.add("✅ Status: Published\n")
  else:
    info.add("❌ Status: Failed\n")
  
  if pubResult.casHash.len > 0:
    info.add(fmt"🔑 CAS Hash: {pubResult.casHash}" & "\n")
  
  if pubResult.signature.len > 0:
    info.add("✍️  Signed: Yes\n")
  
  if pubResult.repoUrl.len > 0:
    info.add(fmt"🌐 Repository URL: {pubResult.repoUrl}" & "\n")
  
  if pubResult.uploadedBytes > 0:
    info.add(fmt"📊 Uploaded: {pubResult.uploadedBytes} bytes" & "\n")
  
  if pubResult.errors.len > 0:
    info.add("⚠️  Errors:\n")
    for err in pubResult.errors:
      info.add("  - " & err & "\n")
  
  return info
