# SPDX-License-Identifier: LSL-1.0
# Copyright (c) 2026 Markus Maiwald
# Stewardship: Self Sovereign Society Foundation
#
# This file is part of the Nexus Sovereign Core.
# See legal/LICENSE_SOVEREIGN.md for license terms.

# nimpak/grafting.nim
# Core grafting infrastructure for external package integration

import std/[tables, sets, strutils, json, os, times, sequtils, hashes, options]
import ./types
import utils/resultutils
import types/grafting_types

type
  # Core grafting engine types
  GraftingEngine* = object
    adapters*: Table[string, PackageAdapter]
    cache*: GraftingCache
    config*: GraftingConfig
    transactions*: seq[GraftTransaction]

# Core grafting engine procedures
proc initGraftingEngine*(configPath: string = ""): Result[GraftingEngine, string] =
  ## Initialize the grafting engine with configuration
  var engine = GraftingEngine(
    adapters: initTable[string, PackageAdapter](),
    cache: GraftingCache(
      cacheDir: getHomeDir() / ".nip" / "graft-cache",
      metadata: initTable[string, GraftedPackageMetadata](),
      archives: initTable[string, string]()
    ),
    config: GraftingConfig(
      enabled: true,
      verifyGraftedPackages: true,
      convertToNpkAutomatically: false,
      adapters: initTable[string, AdapterConfig]()
    ),
    transactions: @[]
  )

  # Load configuration if provided
  if configPath != "":
    let configResult = loadGraftingConfig(configPath)
    if configResult.isErr:
      return err("Failed to load grafting config: " & configResult.error)
    engine.config = configResult.get()

  # Create cache directory
  if not dirExists(engine.cache.cacheDir):
    try:
      createDir(engine.cache.cacheDir)
    except OSError as e:
      return err("Failed to create cache directory: " & e.msg)

  ok(engine)

proc registerAdapter*(engine: var GraftingEngine, adapter: PackageAdapter): Result[void, string] =
  ## Register a package adapter with the grafting engine
  if adapter.name in engine.adapters:
    return err("Adapter already registered: " & adapter.name)

  engine.adapters[adapter.name] = adapter
  echo "Registered grafting adapter: " & adapter.name
  ok()

proc graftPackage*(engine: var GraftingEngine, source: string, packageName: string): Result[GraftResult, string] =
  ## Graft a package from an external source
  if not engine.config.enabled:
    return err("Grafting is disabled in configuration")

  if source notin engine.adapters:
    return err("Unknown grafting source: " & source)

  let adapter = engine.adapters[source]
  if not adapter.enabled:
    return err("Adapter disabled: " & source)

  # Start grafting transaction
  let transaction = GraftTransaction(
    id: "graft-" & $now().toUnix(),
    packageName: packageName,
    adapter: source,
    status: GraftInProgress,
    startTime: now(),
    operations: @[]
  )

  engine.transactions.add(transaction)

  # Delegate to adapter
  let graftResult = adapter.graftPackage(packageName, engine.cache)

  # Update transaction status
  var updatedTransaction = transaction
  updatedTransaction.endTime = some(now())
  updatedTransaction.status = if graftResult.success: GraftCompleted else: GraftFailed

  # Replace transaction in list
  for i, tx in engine.transactions:
    if tx.id == transaction.id:
      engine.transactions[i] = updatedTransaction
      break

  if graftResult.success:
    # Store metadata in cache
    engine.cache.metadata[packageName] = graftResult.metadata
    echo "Successfully grafted package: " & packageName
  else:
    echo "Failed to graft package: " & packageName
    for error in graftResult.errors:
      echo "  Error: " & error

  ok(graftResult)

proc convertGraftedPackage*(engine: var GraftingEngine, packageName: string): Result[string, string] =
  ## Convert a grafted package to .npk format with build hash integration
  if packageName notin engine.cache.metadata:
    return err("Package not found in graft cache: " & packageName)

  let metadata = engine.cache.metadata[packageName]

  # Find extracted path
  let extractedPath = findExtractedPath(engine.cache.cacheDir, packageName)
  if extractedPath == "":
    return err("Extracted package files not found for: " & packageName)

  # Create NPK converter - this will be implemented via forward declaration
  # For now, return a placeholder result
  let npkPath = engine.cache.cacheDir / "npk" / packageName & ".npk"

  # TODO: Implement actual NPK conversion call
  # This will be done through a separate conversion module to avoid circular imports
  echo "✅ Converted grafted package to NPK: " & npkPath
  echo "🔗 Build hash: placeholder-hash"

  ok(npkPath)

proc listGraftedPackages*(engine: GraftingEngine): seq[GraftedPackageMetadata] =
  ## List all grafted packages in cache
  result = @[]
  for metadata in engine.cache.metadata.values:
    result.add(metadata)

proc getGraftingStatus*(engine: GraftingEngine): JsonNode =
  ## Get current grafting engine status
  result = %*{
    "enabled": engine.config.enabled,
    "adapters": {},
    "cache": {
      "directory": engine.cache.cacheDir,
      "packages": engine.cache.metadata.len,
      "archives": engine.cache.archives.len
    },
    "transactions": engine.transactions.len
  }

  for name, adapter in engine.adapters:
    result["adapters"][name] = %*{
      "enabled": adapter.enabled,
      "priority": adapter.priority
    }

# Configuration loading
proc loadGraftingConfig*(path: string): Result[GraftingConfig, string] =
  ## Load grafting configuration from KDL file
  # TODO: Implement KDL parsing for nip-grafting.kdl
  # For now, return default configuration
  ok(GraftingConfig(
    enabled: true,
    verifyGraftedPackages: true,
    convertToNpkAutomatically: false,
    adapters: initTable[string, AdapterConfig]()
  ))

# Base adapter methods (to be overridden by specific adapters)
method graftPackage*(adapter: PackageAdapter, packageName: string, cache: GraftingCache): GraftResult {.base.} =
  ## Base method for grafting a package - must be overridden by specific adapters
  GraftResult(
    success: false,
    errors: @["Base adapter method not implemented"]
  )

method validatePackage*(adapter: PackageAdapter, packageName: string): Result[bool, string] {.base.} =
  ## Base method for validating a package - can be overridden
  ok(true)

method getPackageInfo*(adapter: PackageAdapter, packageName: string): Result[JsonNode, string] {.base.} =
  ## Base method for getting package information - can be overridden
  ok(%*{"name": packageName, "adapter": adapter.name})

# Utility functions
proc calculateGraftHash*(packageName: string, source: string, timestamp: DateTime): string =
  ## Calculate a unique hash for a grafted package
  let input = packageName & "|" & source & "|" & $timestamp.toUnix()
  # TODO: Use proper BLAKE3 hashing when available
  "graft-" & $hash(input)

proc cleanupGraftCache*(engine: var GraftingEngine, olderThan: Duration): Result[int, string] =
  ## Clean up old entries from the graft cache
  var cleaned = 0
  let cutoff = now() - olderThan

  var toRemove: seq[string] = @[]
  for name, metadata in engine.cache.metadata:
    if metadata.graftedAt < cutoff:
      toRemove.add(name)

  for name in toRemove:
    engine.cache.metadata.del(name)
    cleaned += 1

  echo "Cleaned up " & $cleaned & " old graft cache entries"
  ok(cleaned)

proc findExtractedPath(cacheDir: string, packageName: string): string =
  ## Find the extracted path for a grafted package
  let extractedDir = cacheDir / "extracted" / packageName
  if dirExists(extractedDir):
    return extractedDir

  # Try alternative locations
  let builtDir = cacheDir / "built" / packageName
  if dirExists(builtDir):
    return builtDir

  return ""