## NPR Recipe Format Handler (.npr)
##
## This module implements the NPR (Nexus Package Recipe) format for source-level
## package definitions. NPR files are plain-text KDL format files that are
## Git-friendly and contain build instructions, dependencies, and metadata.
##
## Format: .npr (Nexus Package Recipe, plain text KDL)
## - Human-readable KDL format for version control
## - Build instruction templates and dependency specifications
## - Ed25519 digital signatures for recipe integrity
## - Integration with build system and dependency resolution

import std/[os, json, times, strutils, sequtils, tables, options]
import ./types_fixed
import ./formats
import ./cas

type
  NprError* = object of NimPakError
    recipeName*: string

  RecipeValidationResult* = object
    valid*: bool
    errors*: seq[ValidationError]
    warnings*: seq[string]

# =============================================================================
# NPR Recipe Creation and Management
# =============================================================================

proc createNprRecipe*(metadata: Fragment, buildInstructions: BuildTemplate): NprRecipe =
  ## Factory method to create NPR recipe with proper defaults
  NprRecipe(
    metadata: metadata,
    buildInstructions: buildInstructions,
    signature: none(Signature),
    format: NprRecipe,
    cryptoAlgorithms: CryptoAlgorithms(
      hashAlgorithm: "BLAKE2b",
      signatureAlgorithm: "Ed25519",
      version: "1.0"
    )
  )

proc createBuildTemplate*(system: BuildSystemType,
                         configureArgs: seq[string] = @[],
                         buildArgs: seq[string] = @[],
                         installArgs: seq[string] = @[],
                         environment: Table[string, string] = initTable[string, string]()): BuildTemplate =
  ## Factory method to create build template with sensible defaults
  BuildTemplate(
    system: system,
    configureArgs: configureArgs,
    buildArgs: buildArgs,
    installArgs: installArgs,
    environment: environment
  )

# =============================================================================
# KDL Serialization for NPR Format
# =============================================================================

proc escapeKdlString(s: string): string =
  ## Escape special characters in KDL strings
  result = "\""
  for c in s:
    case c:
    of '"': result.add("\\\"")
    of '\\': result.add("\\\\")
    of '\n': result.add("\\n")
    of '\r': result.add("\\r")
    of '\t': result.add("\\t")
    else: result.add(c)
  result.add("\"")

proc formatKdlBoolean(b: bool): string =
  ## Format boolean for KDL
  if b: "true" else: "false"

proc formatKdlArray(items: seq[string]): string =
  ## Format string array for KDL
  if items.len == 0:
    return ""
  result = ""
  for i, item in items:
    if i > 0: result.add(" ")
    result.add(escapeKdlString(item))

proc formatKdlTable(table: Table[string, string]): string =
  ## Format table as KDL key-value pairs
  result = ""
  for key, value in table:
    result.add("    " & escapeKdlString(key) & " " & escapeKdlString(value) & "\n")

proc toHex(b: byte): string =
  ## Convert byte to hex string
  const hexChars = "0123456789abcdef"
  result = $hexChars[b shr 4] & $hexChars[b and 0x0F]

proc serializeNprToKdl*(recipe: NprRecipe): string =
  ## Serialize NPR recipe to KDL format with comprehensive metadata
  ## Plain-text format optimized for Git version control and human readability

  result = "recipe " & escapeKdlString(recipe.metadata.id.name) & " {\n"
  result.add("  version " & escapeKdlString(recipe.metadata.id.version) & "\n")
  result.add("  stream " & escapeKdlString($recipe.metadata.id.stream) & "\n")
  result.add("  format " & escapeKdlString($recipe.format) & "\n")
  result.add("\n")

  # Source information for fetching and building
  result.add("  source {\n")
  result.add("    method " & escapeKdlString($recipe.metadata.source.sourceMethod) & "\n")
  result.add("    url " & escapeKdlString(recipe.metadata.source.url) & "\n")
  result.add("    hash " & escapeKdlString(recipe.metadata.source.hash) & "\n")
  result.add("    hash-algorithm " & escapeKdlString(recipe.metadata.source.hashAlgorithm) & "\n")
  result.add("    timestamp " & escapeKdlString($recipe.metadata.source.timestamp) & "\n")
  result.add("  }\n\n")

  # Build system configuration and instructions
  result.add("  build {\n")
  result.add("    system " & escapeKdlString($recipe.buildInstructions.system) & "\n")

  if recipe.buildInstructions.configureArgs.len > 0:
    result.add("    configure-args " & formatKdlArray(recipe.buildInstructions.configureArgs) & "\n")

  if recipe.buildInstructions.buildArgs.len > 0:
    result.add("    build-args " & formatKdlArray(recipe.buildInstructions.buildArgs) & "\n")

  if recipe.buildInstructions.installArgs.len > 0:
    result.add("    install-args " & formatKdlArray(recipe.buildInstructions.installArgs) & "\n")

  if recipe.buildInstructions.environment.len > 0:
    result.add("    environment {\n")
    result.add(formatKdlTable(recipe.buildInstructions.environment))
    result.add("    }\n")

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

  # Package metadata
  result.add("  metadata {\n")
  result.add("    description " & escapeKdlString(recipe.metadata.metadata.description) & "\n")
  result.add("    license " & escapeKdlString(recipe.metadata.metadata.license) & "\n")
  result.add("    maintainer " & escapeKdlString(recipe.metadata.metadata.maintainer) & "\n")
  if recipe.metadata.metadata.tags.len > 0:
    result.add("    tags " & formatKdlArray(recipe.metadata.metadata.tags) & "\n")
  result.add("  }\n\n")

  # Runtime profile requirements
  result.add("  runtime {\n")
  result.add("    libc " & escapeKdlString($recipe.metadata.metadata.runtime.libc) & "\n")
  result.add("    allocator " & escapeKdlString($recipe.metadata.metadata.runtime.allocator) & "\n")
  result.add("    systemd-aware " & formatKdlBoolean(recipe.metadata.metadata.runtime.systemdAware) & "\n")
  result.add("    reproducible " & formatKdlBoolean(recipe.metadata.metadata.runtime.reproducible) & "\n")
  if recipe.metadata.metadata.runtime.tags.len > 0:
    result.add("    tags " & formatKdlArray(recipe.metadata.metadata.runtime.tags) & "\n")
  result.add("  }\n\n")

  # Dependencies with version constraints
  if recipe.metadata.dependencies.len > 0:
    result.add("  dependencies {\n")
    for dep in recipe.metadata.dependencies:
      result.add("    " & escapeKdlString(dep.name) & " " & escapeKdlString(dep.version) & " stream=" & escapeKdlString($dep.stream) & "\n")
    result.add("  }\n\n")

  # ACUL compliance requirements
  result.add("  acul {\n")
  result.add("    required " & formatKdlBoolean(recipe.metadata.acul.required) & "\n")
  if recipe.metadata.acul.membership.len > 0:
    result.add("    membership " & escapeKdlString(recipe.metadata.acul.membership) & "\n")
  if recipe.metadata.acul.attribution.len > 0:
    result.add("    attribution " & escapeKdlString(recipe.metadata.acul.attribution) & "\n")
  if recipe.metadata.acul.buildLog.len > 0:
    result.add("    build-log " & escapeKdlString(recipe.metadata.acul.buildLog) & "\n")
  result.add("  }\n\n")

  # Cryptographic integrity and signature
  result.add("  integrity {\n")
  result.add("    algorithm " & escapeKdlString(recipe.cryptoAlgorithms.hashAlgorithm) & "\n")
  result.add("    signature-algorithm " & escapeKdlString(recipe.cryptoAlgorithms.signatureAlgorithm) & "\n")
  result.add("    version " & escapeKdlString(recipe.cryptoAlgorithms.version) & "\n")
  if recipe.signature.isSome:
    let sig = recipe.signature.get()
    result.add("    signature " & escapeKdlString(sig.signature.mapIt(it.toHex()).join("")) & "\n")
    result.add("    key-id " & escapeKdlString(sig.keyId) & "\n")
  result.add("  }\n")

  result.add("}\n")

proc deserializeNprFromKdl*(kdlContent: string): Result[NprRecipe, NprError] =
  ## Deserialize NPR recipe from KDL format
  ## TODO: Implement proper KDL parsing when kdl library is available
  ## For now, return an error indicating this is not yet implemented
  return err[NprRecipe, NprError](NprError(
    code: InvalidMetadata,
    msg: "KDL deserialization not yet implemented - waiting for kdl library",
    recipeName: "unknown"
  ))

# =============================================================================
# Recipe Validation
# =============================================================================

proc validateNprRecipe*(recipe: NprRecipe): RecipeValidationResult =
  ## Validate NPR recipe format and content
  var result = RecipeValidationResult(valid: true, errors: @[], warnings: @[])

  # Validate basic metadata
  if recipe.metadata.id.name.len == 0:
    result.errors.add(ValidationError(
      field: "metadata.id.name",
      message: "Recipe name cannot be empty",
      suggestions: @["Provide a valid recipe name"]
    ))
    result.valid = false

  if recipe.metadata.id.version.len == 0:
    result.errors.add(ValidationError(
      field: "metadata.id.version",
      message: "Recipe version cannot be empty",
      suggestions: @["Provide a valid version string"]
    ))
    result.valid = false

  # Validate source information
  if recipe.metadata.source.url.len == 0:
    result.errors.add(ValidationError(
      field: "metadata.source.url",
      message: "Source URL cannot be empty",
      suggestions: @["Provide a valid source URL"]
    ))
    result.valid = false

  if recipe.metadata.source.hash.len == 0:
    result.errors.add(ValidationError(
      field: "metadata.source.hash",
      message: "Source hash cannot be empty",
      suggestions: @["Calculate and provide source hash"]
    ))
    result.valid = false

  # Validate build system configuration
  if recipe.buildInstructions.system == Custom and recipe.buildInstructions.configureArgs.len == 0:
    result.warnings.add("Custom build system without configure arguments may require manual intervention")

  # Validate build system specific requirements
  case recipe.buildInstructions.system:
  of CMake:
    if "CMAKE_BUILD_TYPE" notin recipe.buildInstructions.environment:
      result.warnings.add("CMake build without CMAKE_BUILD_TYPE may use debug configuration")
  of Autotools:
    if recipe.buildInstructions.configureArgs.len == 0:
      result.warnings.add("Autotools build without configure arguments may use default configuration")
  of Cargo:
    if "CARGO_BUILD_TARGET" notin recipe.buildInstructions.environment:
      result.warnings.add("Cargo build without explicit target may not be reproducible")
  else:
    discard

  # Validate dependencies
  for i, dep in recipe.metadata.dependencies:
    if dep.name.len == 0:
      result.errors.add(ValidationError(
        field: "dependencies[" & $i & "].name",
        message: "Dependency name cannot be empty",
        suggestions: @["Provide valid dependency name"]
      ))
      result.valid = false

    if dep.version.len == 0:
      result.errors.add(ValidationError(
        field: "dependencies[" & $i & "].version",
        message: "Dependency version cannot be empty",
        suggestions: @["Provide valid dependency version"]
      ))
      result.valid = false

  # Validate cryptographic algorithms
  if not isQuantumResistant(recipe.cryptoAlgorithms):
    result.warnings.add("Using non-quantum-resistant algorithms: " &
                       recipe.cryptoAlgorithms.hashAlgorithm & "/" &
                       recipe.cryptoAlgorithms.signatureAlgorithm)

  return result

# =============================================================================
# Recipe File Operations
# =============================================================================

proc saveNprRecipe*(recipe: NprRecipe, filePath: string): Result[void, NprError] =
  ## Save NPR recipe to file in KDL format
  try:
    let kdlContent = serializeNprToKdl(recipe)

    # Ensure the file has the correct .npr extension
    let finalPath = if filePath.endsWith(".npr"): filePath else: filePath & ".npr"

    # Ensure parent directory exists
    let parentDir = finalPath.parentDir()
    if not dirExists(parentDir):
      createDir(parentDir)

    writeFile(finalPath, kdlContent)
    return ok[void, NprError]()

  except IOError as e:
    return err[void, NprError](NprError(
      code: FileWriteError,
      msg: "Failed to save NPR recipe: " & e.msg,
      recipeName: recipe.metadata.id.name
    ))

proc loadNprRecipe*(filePath: string): Result[NprRecipe, NprError] =
  ## Load NPR recipe from file
  try:
    if not fileExists(filePath):
      return err[NprRecipe, NprError](NprError(
        code: PackageNotFound,
        msg: "NPR recipe file not found: " & filePath,
        recipeName: "unknown"
      ))

    let kdlContent = readFile(filePath)
    return deserializeNprFromKdl(kdlContent)

  except IOError as e:
    return err[NprRecipe, NprError](NprError(
      code: FileReadError,
      msg: "Failed to load NPR recipe: " & e.msg,
      recipeName: "unknown"
    ))

# =============================================================================
# Recipe Digital Signatures
# =============================================================================

proc signNprRecipe*(recipe: var NprRecipe, keyId: string, privateKey: seq[byte]): Result[void, NprError] =
  ## Sign NPR recipe with Ed25519 private key
  ## Creates a comprehensive signature payload including all critical recipe metadata
  try:
    # Create comprehensive signature payload from recipe metadata and build instructions
    let payload = recipe.metadata.id.name &
                 recipe.metadata.id.version &
                 $recipe.metadata.id.stream &
                 recipe.metadata.source.hash &
                 $recipe.buildInstructions.system &
                 recipe.buildInstructions.configureArgs.join(" ") &
                 recipe.buildInstructions.buildArgs.join(" ") &
                 recipe.buildInstructions.installArgs.join(" ")

    # TODO: Implement actual Ed25519 signing when crypto library is available
    # For now, create a deterministic placeholder signature based on payload
    let payloadHash = calculateBlake2b(payload.toOpenArrayByte(0, payload.len - 1).toSeq())
    let placeholderSig = payloadHash[0..63].toOpenArrayByte(0, 63).toSeq()  # 64 bytes like Ed25519

    let signature = Signature(
      keyId: keyId,
      algorithm: recipe.cryptoAlgorithms.signatureAlgorithm,
      signature: placeholderSig
    )

    recipe.signature = some(signature)
    return ok[void, NprError]()

  except Exception as e:
    return err[void, NprError](NprError(
      code: UnknownError,
      msg: "Failed to sign recipe: " & e.msg,
      recipeName: recipe.metadata.id.name
    ))

proc verifyNprSignature*(recipe: NprRecipe, publicKey: seq[byte]): Result[bool, NprError] =
  ## Verify NPR recipe signature
  ## TODO: Implement proper Ed25519 verification when crypto library is available
  if recipe.signature.isNone:
    return ok[bool, NprError](false)  # No signature to verify

  try:
    let sig = recipe.signature.get()

    # TODO: Implement actual Ed25519 verification
    # For now, just check if signature exists and has correct length
    let isValid = sig.signature.len == 64 and sig.keyId.len > 0

    return ok[bool, NprError](isValid)

  except Exception as e:
    return err[bool, NprError](NprError(
      code: UnknownError,
      msg: "Failed to verify signature: " & e.msg,
      recipeName: recipe.metadata.id.name
    ))

# =============================================================================
# Build System Integration
# =============================================================================

proc getBuildSystemDefaults*(system: BuildSystemType): BuildTemplate =
  ## Get default build template for a build system
  case system:
  of CMake:
    return createBuildTemplate(
      system = CMake,
      configureArgs = @["-DCMAKE_BUILD_TYPE=Release"],
      buildArgs = @["--build", ".", "--parallel"],
      installArgs = @["--install", "."],
      environment = {"CMAKE_BUILD_TYPE": "Release"}.toTable()
    )
  of Autotools:
    return createBuildTemplate(
      system = Autotools,
      configureArgs = @["--prefix=/usr"],
      buildArgs = @["-j$(nproc)"],
      installArgs = @["install"],
      environment = initTable[string, string]()
    )
  of Meson:
    return createBuildTemplate(
      system = Meson,
      configureArgs = @["setup", "builddir", "--buildtype=release"],
      buildArgs = @["-C", "builddir"],
      installArgs = @["install", "-C", "builddir"],
      environment = initTable[string, string]()
    )
  of Cargo:
    return createBuildTemplate(
      system = Cargo,
      configureArgs = @[],
      buildArgs = @["build", "--release"],
      installArgs = @["install", "--path", "."],
      environment = {"CARGO_BUILD_TARGET": "x86_64-unknown-linux-musl"}.toTable()
    )
  of NimBuild:
    return createBuildTemplate(
      system = NimBuild,
      configureArgs = @[],
      buildArgs = @["c", "-d:release", "--mm:orc"],
      installArgs = @[],
      environment = {"NIM_BUILD_TYPE": "release"}.toTable()
    )
  of Custom:
    return createBuildTemplate(
      system = Custom,
      configureArgs = @[],
      buildArgs = @[],
      installArgs = @[],
      environment = initTable[string, string]()
    )

proc validateBuildInstructions*(instructions: BuildTemplate): seq[string] =
  ## Validate build instructions and return warnings
  var warnings: seq[string] = @[]

  case instructions.system:
  of CMake:
    if "-DCMAKE_BUILD_TYPE" notin instructions.configureArgs.join(" "):
      warnings.add("CMake build without explicit build type")
  of Autotools:
    if "--prefix" notin instructions.configureArgs.join(" "):
      warnings.add("Autotools build without explicit prefix")
  of Cargo:
    if "--release" notin instructions.buildArgs.join(" "):
      warnings.add("Cargo build without release flag")
  else:
    discard

  return warnings

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

proc getNprInfo*(recipe: NprRecipe): string =
  ## Get human-readable recipe information
  result = "NPR Recipe: " & recipe.metadata.id.name & " v" & recipe.metadata.id.version & "\n"
  result.add("Stream: " & $recipe.metadata.id.stream & "\n")
  result.add("Build System: " & $recipe.buildInstructions.system & "\n")
  result.add("Dependencies: " & $recipe.metadata.dependencies.len & "\n")
  result.add("Source: " & recipe.metadata.source.url & "\n")
  if recipe.signature.isSome:
    result.add("Signed: Yes (Key: " & recipe.signature.get().keyId & ")\n")
  else:
    result.add("Signed: No\n")

proc calculateBlake2b*(data: seq[byte]): string =
  ## Calculate BLAKE2b hash - imported from CAS module
  cas.calculateBlake2b(data)

proc calculateBlake3*(data: seq[byte]): string =
  ## Calculate BLAKE3 hash - imported from CAS module
  cas.calculateBlake3(data)