# nimpak/npk_conversion.nim
# Enhanced NPK conversion with build hash integration

import std/[strutils, json, os, times, tables, sequtils, strformat, algorithm, osproc]
import ../nip/types
import utils/resultutils
import types/grafting_types

type
  NPKConverter* = object
    outputDir*: string
    compressionLevel*: int
    includeProvenance*: bool
    calculateBuildHash*: bool
    signPackages*: bool
    keyPath*: string

  BuildConfiguration* = object
    sourceHash*: string
    sourceTimestamp*: DateTime
    configureFlags*: seq[string]
    compilerFlags*: seq[string]
    linkerFlags*: seq[string]
    compilerVersion*: string
    nimVersion*: string
    nimFlags*: seq[string]
    targetArchitecture*: string
    libc*: string
    libcVersion*: string
    allocator*: string
    allocatorVersion*: string
    environmentVars*: Table[string, string]
    dependencies*: seq[DependencyHash]

  DependencyHash* = object
    packageName*: string
    buildHash*: string

  BuildHash* = object
    hash*: string
    algorithm*: string
    components*: seq[string]
    timestamp*: DateTime

  NPKManifest* = object
    name*: string
    version*: string
    description*: string
    homepage*: string
    license*: seq[string]
    maintainer*: string
    buildHash*: string
    sourceHash*: string
    artifactHash*: string
    buildConfig*: BuildConfiguration
    dependencies*: seq[DependencyHash]
    acul*: AculCompliance
    files*: seq[NPKFile]
    provenance*: ProvenanceInfo
    created*: DateTime
    converterName*: string

  NPKFile* = object
    path*: string
    hash*: string
    permissions*: string
    size*: int64

  ConversionResult* = object
    success*: bool
    npkPath*: string
    manifest*: NPKManifest
    buildHash*: BuildHash
    errors*: seq[string]

# Hash-relevant environment variables (from SHARED_SPECIFICATIONS.md)
const HASH_RELEVANT_ENV_VARS* = [
  "CC", "CXX", "CFLAGS", "CXXFLAGS", "LDFLAGS", "PKG_CONFIG_PATH",
  "PATH", "LD_LIBRARY_PATH", "MAKEFLAGS", "DESTDIR"
]

proc newNPKConverter*(outputDir: string = ""): NPKConverter =
  ## Create a new NPK converter
  NPKConverter(
    outputDir: if outputDir == "": getTempDir() / "nimpak-npk" else: outputDir,
    compressionLevel: 6,
    includeProvenance: true,
    calculateBuildHash: true,
    signPackages: false,
    keyPath: ""
  )

proc convertToNPK*(conv: NPKConverter, metadata: GraftedPackageMetadata,
                   extractedPath: string): Result[ConversionResult, string] =
  ## Convert a grafted package to NPK format with build hash integration
  echo fmt"🔄 Converting {metadata.packageName} to NPK format..."

  var result = ConversionResult(success: false, errors: @[])

  try:
    # Create output directory
    if not dirExists(conv.outputDir):
      createDir(conv.outputDir)

    # Generate NPK manifest
    let manifestResult = generateNPKManifest(conv, metadata, extractedPath)
    if manifestResult.isErr:
      result.errors.add("Failed to generate manifest: " & manifestResult.error)
      return ok(result)

    let manifest = manifestResult.get()
    result.manifest = manifest

    # Calculate build hash if enabled
    if conv.calculateBuildHash:
      let buildHashResult = calculateBuildHash(manifest.buildConfig)
      if buildHashResult.isErr:
        result.errors.add("Failed to calculate build hash: " & buildHashResult.error)
        return ok(result)

      result.buildHash = buildHashResult.get()
      result.manifest.buildHash = result.buildHash.hash

    # Create NPK package
    let npkResult = createNPKPackage(conv, manifest, extractedPath)
    if npkResult.isErr:
      result.errors.add("Failed to create NPK package: " & npkResult.error)
      return ok(result)

    result.npkPath = npkResult.get()
    result.success = true

    echo fmt"✅ Successfully converted to NPK: {result.npkPath}"

  except Exception as e:
    result.errors.add(fmt"Exception during conversion: {e.msg}")

  ok(result)

proc generateNPKManifest(conv: NPKConverter, metadata: GraftedPackageMetadata,
                        extractedPath: string): Result[NPKManifest, string] =
  ## Generate NPK manifest from grafted package metadata
  try:
    # Scan files in extracted path
    let filesResult = scanPackageFiles(extractedPath)
    if filesResult.isErr:
      return err("Failed to scan package files: " & filesResult.error)

    let files = filesResult.get()

    # Extract build configuration from metadata
    let buildConfig = extractBuildConfiguration(metadata)

    # Calculate artifact hash
    let artifactHash = calculateArtifactHash(files)

    let manifest = NPKManifest(
      name: metadata.packageName,
      version: metadata.version,
      description: extractDescription(metadata),
      homepage: extractHomepage(metadata),
      license: extractLicense(metadata),
      maintainer: extractMaintainer(metadata),
      buildHash: "",  # Will be filled later
      sourceHash: metadata.originalHash,
      artifactHash: artifactHash,
      buildConfig: buildConfig,
      dependencies: extractDependencies(metadata),
      acul: AculCompliance(
        required: false,
        membership: "NexusOS-Community",
        attribution: fmt"Grafted from {metadata.source}",
        buildLog: metadata.buildLog
      ),
      files: files,
      provenance: metadata.provenance,
      created: now(),
      converterName: "nimpak-" & metadata.source
    )

    ok(manifest)

  except Exception as e:
    err(fmt"Exception generating manifest: {e.msg}")

proc scanPackageFiles(extractedPath: string): Result[seq[NPKFile], string] =
  ## Scan extracted package directory and create file manifest
  var files: seq[NPKFile] = @[]

  try:
    if not dirExists(extractedPath):
      return err(fmt"Extracted path does not exist: {extractedPath}")

    # Walk through all files
    for file in walkDirRec(extractedPath):
      let relativePath = file.replace(extractedPath, "").replace("\\", "/")
      if relativePath.startsWith("/"):
        let cleanPath = relativePath[1..^1]
      else:
        let cleanPath = relativePath

      if fileExists(file):
        let info = getFileInfo(file)
        let hash = calculateFileHash(file)

        files.add(NPKFile(
          path: "/" & cleanPath,
          hash: hash,
          permissions: getFilePermissions(file),
          size: info.size
        ))

    ok(files)

  except Exception as e:
    err(fmt"Exception scanning files: {e.msg}")

proc extractBuildConfiguration(metadata: GraftedPackageMetadata): BuildConfiguration =
  ## Extract build configuration from grafted package metadata
  BuildConfiguration(
    sourceHash: metadata.originalHash,
    sourceTimestamp: metadata.graftedAt,
    configureFlags: extractConfigureFlags(metadata),
    compilerFlags: extractCompilerFlags(metadata),
    linkerFlags: @[],
    compilerVersion: extractCompilerVersion(metadata),
    nimVersion: "2.0.0",  # TODO: Get actual Nim version
    nimFlags: @[],
    targetArchitecture: "x86_64",  # TODO: Detect actual architecture
    libc: detectLibc(metadata),
    libcVersion: detectLibcVersion(metadata),
    allocator: "default",
    allocatorVersion: "system",
    environmentVars: extractEnvironmentVars(metadata),
    dependencies: extractDependencies(metadata)
  )

proc calculateBuildHash*(config: BuildConfiguration): Result[BuildHash, string] =
  ## Calculate build hash using the shared algorithm from SHARED_SPECIFICATIONS.md
  try:
    var components: seq[string] = @[]

    # 1. Source integrity (sorted deterministically)
    components.add(config.sourceHash)
    components.add(config.sourceTimestamp.format("yyyy-MM-dd'T'HH:mm:ss'Z'"))

    # 2. Build configuration (sorted alphabetically)
    components.add(config.configureFlags.sorted().join(" "))
    components.add(config.compilerFlags.sorted().join(" "))
    components.add(config.linkerFlags.sorted().join(" "))

    # 3. Toolchain fingerprint
    components.add(config.compilerVersion)
    components.add(config.nimVersion & " " & config.nimFlags.sorted().join(" "))

    # 4. Target environment
    components.add(config.targetArchitecture)
    components.add(config.libc & "-" & config.libcVersion)
    components.add(config.allocator & "-" & config.allocatorVersion)

    # 5. Environment variables (filtered and sorted)
    let envVars = config.environmentVars.keys.toSeq.sorted()
    for key in envVars:
      if key in HASH_RELEVANT_ENV_VARS:
        components.add(key & "=" & config.environmentVars[key])

    # 6. Dependency hashes (sorted by package name)
    let sortedDeps = config.dependencies.sortedByIt(it.packageName)
    for dep in sortedDeps:
      components.add(dep.packageName & ":" & dep.buildHash)

    # Calculate final hash
    let input = components.join("|")
    let hash = blake3Hash(input)

    ok(BuildHash(
      hash: hash,
      algorithm: "blake3",
      components: components,
      timestamp: now()
    ))

  except Exception as e:
    err(fmt"Exception calculating build hash: {e.msg}")

proc blake3Hash(input: string): string =
  ## Calculate BLAKE3 hash (placeholder implementation)
  # TODO: Use actual BLAKE3 when available
  "blake3-" & $hash(input)

proc createNPKPackage(conv: NPKConverter, manifest: NPKManifest,
                     extractedPath: string): Result[string, string] =
  ## Create the actual NPK package file
  try:
    let npkPath = conv.outputDir / fmt"{manifest.name}-{manifest.version}.npk"

    # Create manifest file
    let manifestPath = conv.outputDir / "manifest.kdl"
    let manifestResult = writeManifestKDL(manifest, manifestPath)
    if manifestResult.isErr:
      return err("Failed to write manifest: " & manifestResult.error)

    # Create files archive
    let filesArchivePath = conv.outputDir / "files.tar.zst"
    let archiveResult = createFilesArchive(extractedPath, filesArchivePath, conv.compressionLevel)
    if archiveResult.isErr:
      return err("Failed to create files archive: " & archiveResult.error)

    # Create build log file
    let buildLogPath = conv.outputDir / "build.log"
    writeFile(buildLogPath, manifest.provenance.conversionLog)

    # Create final NPK package
    let createCmd = fmt"tar -czf {npkPath} -C {conv.outputDir} manifest.kdl files.tar.zst build.log"
    let (output, exitCode) = execCmdEx(createCmd)

    if exitCode != 0:
      return err(fmt"Failed to create NPK package: {output}")

    # Sign package if enabled
    if conv.signPackages and conv.keyPath != "":
      let signResult = signNPKPackage(npkPath, conv.keyPath)
      if signResult.isErr:
        echo fmt"⚠️  Warning: Failed to sign package: {signResult.error}"

    ok(npkPath)

  except Exception as e:
    err(fmt"Exception creating NPK package: {e.msg}")

proc writeManifestKDL(manifest: NPKManifest, outputPath: string): Result[void, string] =
  ## Write NPK manifest in KDL format
  try:
    var kdl = fmt"""// NPK Package Manifest
package "{manifest.name}" {{
  version "{manifest.version}"
  description "{manifest.description}"

  // Build configuration and hashes
  build_hash "{manifest.buildHash}"
  source_hash "{manifest.sourceHash}"
  artifact_hash "{manifest.artifactHash}"

  build_config {{
    configure_flags {formatStringArray(manifest.buildConfig.configureFlags)}
    compiler_flags {formatStringArray(manifest.buildConfig.compilerFlags)}
    target_architecture "{manifest.buildConfig.targetArchitecture}"
    libc "{manifest.buildConfig.libc}-{manifest.buildConfig.libcVersion}"
    allocator "{manifest.buildConfig.allocator}-{manifest.buildConfig.allocatorVersion}"
  }}

  // Dependencies with their build hashes
  dependencies {{
"""

    for dep in manifest.dependencies:
      kdl.add(fmt"""    {dep.packageName} {{
      build_hash "{dep.buildHash}"
    }}
""")

    kdl.add(fmt"""  }}

  // ACUL compliance metadata
  acul {{
    required {manifest.acul.required}
    membership "{manifest.acul.membership}"
    license "{manifest.license.join(", ")}"
    attribution "{manifest.acul.attribution}"
  }}

  // File manifest
  files {{
""")

    for file in manifest.files:
      kdl.add(fmt"""    "{file.path}" {{
      hash "{file.hash}"
      permissions "{file.permissions}"
      size {file.size}
    }}
""")

    kdl.add(fmt"""  }}

  // Provenance information
  provenance {{
    original_source "{manifest.provenance.originalSource}"
    download_url "{manifest.provenance.downloadUrl}"
    converted_at "{manifest.created}"
    converter "{manifest.converterName}"
  }}
}}
""")

    writeFile(outputPath, kdl)
    ok()

  except Exception as e:
    err(fmt"Exception writing manifest: {e.msg}")

# Helper functions for metadata extraction
proc extractDescription(metadata: GraftedPackageMetadata): string =
  # TODO: Extract from build log or metadata
  fmt"Package {metadata.packageName} grafted from {metadata.source}"

proc extractHomepage(metadata: GraftedPackageMetadata): string =
  # TODO: Extract from package metadata
  ""

proc extractLicense(metadata: GraftedPackageMetadata): seq[string] =
  # TODO: Extract from package metadata
  @["unknown"]

proc extractMaintainer(metadata: GraftedPackageMetadata): string =
  # TODO: Extract from package metadata
  "NimPak Grafting System"

proc extractConfigureFlags(metadata: GraftedPackageMetadata): seq[string] =
  # TODO: Parse from build log
  @[]

proc extractCompilerFlags(metadata: GraftedPackageMetadata): seq[string] =
  # TODO: Parse from build log
  @["-O2"]

proc extractCompilerVersion(metadata: GraftedPackageMetadata): string =
  # TODO: Detect from system
  "gcc-11.0"

proc detectLibc(metadata: GraftedPackageMetadata): string =
  # TODO: Detect actual libc
  "musl"

proc detectLibcVersion(metadata: GraftedPackageMetadata): string =
  # TODO: Detect actual version
  "1.2.4"

proc extractEnvironmentVars(metadata: GraftedPackageMetadata): Table[string, string] =
  # TODO: Extract from build environment
  initTable[string, string]()

proc extractDependencies(metadata: GraftedPackageMetadata): seq[DependencyHash] =
  # TODO: Extract from package metadata
  @[]

proc calculateArtifactHash(files: seq[NPKFile]): string =
  ## Calculate hash of all package files
  var input = ""
  for file in files.sortedByIt(it.path):
    input.add(file.path & ":" & file.hash & ":" & $file.size & "|")
  "artifact-" & $hash(input)

proc calculateFileHash(filePath: string): string =
  ## Calculate hash of individual file
  try:
    # TODO: Use actual BLAKE3 when available
    let content = readFile(filePath)
    "file-" & $hash(content)
  except:
    "file-hash-error"

proc getFilePermissions(filePath: string): string =
  ## Get file permissions as string
  try:
    let info = getFileInfo(filePath)
    # TODO: Convert FilePermissions to octal string
    "644"  # Default for now
  except:
    "644"

proc createFilesArchive(sourcePath: string, archivePath: string, compressionLevel: int): Result[void, string] =
  ## Create compressed archive of package files
  try:
    let cmd = fmt"tar -cf - -C {sourcePath} . | zstd -{compressionLevel} -o {archivePath}"
    let (output, exitCode) = execCmdEx(cmd)

    if exitCode != 0:
      return err(fmt"Archive creation failed: {output}")

    ok()

  except Exception as e:
    err(fmt"Exception creating archive: {e.msg}")

proc signNPKPackage(npkPath: string, keyPath: string): Result[void, string] =
  ## Sign NPK package with cryptographic signature
  try:
    # TODO: Implement actual signing with Ed25519
    let signaturePath = npkPath & ".sig"
    writeFile(signaturePath, "placeholder-signature")
    ok()

  except Exception as e:
    err(fmt"Exception signing package: {e.msg}")

proc formatStringArray(arr: seq[string]): string =
  ## Format string array for KDL output
  if arr.len == 0:
    return "\"\""

  var result = ""
  for i, item in arr:
    if i > 0:
      result.add(" ")
    result.add(fmt"\"{item}\"")
  result