# nimpak/adapters/nix.nim
# Nix grafting adapter for accessing the Nix package ecosystem

import std/[strutils, json, os, times, osproc, tables, strformat]
import ../grafting
import ../types

type
  NixAdapter* = ref object of PackageAdapter
    nixpkgsChannel*: string
    cacheDir*: string
    useBinaryCache*: bool
    nixStorePath*: string
    buildInSandbox*: bool
    allowUnfree*: bool

  NixPackageInfo* = object
    name*: string
    version*: string
    description*: string
    homepage*: string
    license*: seq[string]
    maintainers*: seq[string]
    platforms*: seq[string]
    outputs*: seq[string]
    derivation*: string
    storePath*: string

  NixDerivation* = object
    name*: string
    system*: string
    builder*: string
    args*: seq[string]
    env*: Table[string, string]
    inputDrvs*: Table[string, seq[string]]
    inputSrcs*: seq[string]
    outputs*: Table[string, string]

  NixBuildResult = object
    success: bool
    storePath: string
    buildLog: string
    error: string

  NixExtractResult = object
    success: bool
    extractedPath: string
    conversionLog: string
    error: string

# Forward declarations
proc isNixAvailable(): bool
proc getNixPackageInfo(adapter: NixAdapter, packageName: string): NixPackageInfo
proc searchNixPackage(adapter: NixAdapter, packageName: string): NixPackageInfo
proc buildNixPackage(adapter: NixAdapter, packageName: string, info: NixPackageInfo): NixBuildResult
proc extractFromNixStore(adapter: NixAdapter, storePath: string, packageName: string, cache: GraftingCache): NixExtractResult
proc calculateNixStoreHash(storePath: string): string

proc newNixAdapter*(config: JsonNode = nil): NixAdapter =
  ## Create a new Nix adapter with configuration
  result = NixAdapter(
    name: "nix",
    priority: 20,
    enabled: true,
    nixpkgsChannel: "nixos-unstable",
    cacheDir: "/var/cache/nip/nix",
    useBinaryCache: true,
    nixStorePath: "/nix/store",
    buildInSandbox: true,
    allowUnfree: false
  )

  # Apply configuration if provided
  if config != nil:
    if config.hasKey("channel"):
      result.nixpkgsChannel = config["channel"].getStr()
    if config.hasKey("cache_dir"):
      result.cacheDir = config["cache_dir"].getStr()
    if config.hasKey("use_binary_cache"):
      result.useBinaryCache = config["use_binary_cache"].getBool()
    if config.hasKey("nix_store_path"):
      result.nixStorePath = config["nix_store_path"].getStr()
    if config.hasKey("build_in_sandbox"):
      result.buildInSandbox = config["build_in_sandbox"].getBool()
    if config.hasKey("allow_unfree"):
      result.allowUnfree = config["allow_unfree"].getBool()

method graftPackage*(adapter: NixAdapter, packageName: string, cache: GraftingCache): GraftResult =
  ## Graft a package from Nix
  echo fmt"🌱 Grafting package from Nix: {packageName}"

  result = GraftResult(
    success: false,
    packageId: packageName,
    errors: @[]
  )

  try:
    # First, check if Nix is available
    if not isNixAvailable():
      result.errors.add("Nix is not installed or notle in PATH")
      return result

    # Get package information from nixpkgs
    let packageInfo = getNixPackageInfo(adapter, packageName)
    if packageInfo.name == "":
      result.errors.add(fmt"Package '{packageName}' not found in nixpkgs")
      return result

    # Build the package using nix-build
    let buildResult = buildNixPackage(adapter, packageName, packageInfo)
    if not buildResult.success:
      result.errors.add("Failed to build Nix package: " & buildResult.error)
      return result

    # Extract package from Nix store to our structure
    let extractResult = extractFromNixStore(adapter, buildResult.storePath, packageName, cache)
    if not extractResult.success:
      result.errors.add("Failed to extract from Nix store: " & extractResult.error)
      return result

    # Calculate hashes
    let originalHash = calculateNixStoreHash(buildResult.storePath)
    let graftHash = calculateGraftHash(packageName, "nix", now())

    # Create metadata
    let metadata = GraftedPackageMetadata(
      packageName: packageInfo.name,
      version: packageInfo.version,
      source: "nix",
      graftedAt: now(),
      originalHash: originalHash,
      graftHash: graftHash,
      buildLog: buildResult.buildLog,
      provenance: ProvenanceInfo(
        originalSource: "nixpkgs-" & adapter.nixpkgsChannel,
        downloadUrl: fmt"https://github.com/NixOS/nixpkgs/tree/{adapter.nixpkgsChannel}",
        archivePath: buildResult.storePath,
        extractedPath: extractResult.extractedPath,
        conversionLog: extractResult.conversionLog
      )
    )

    result.success = true
    result.packageId = packageInfo.name
    result.metadata = metadata

    echo fmt"✅ Successfully grafted from Nix: {packageInfo.name} {packageInfo.version}"
    echo fmt"📍 Store path: {buildResult.storePath}"

  except Exception as e:
    result.errors.add(fmt"Exception in Nix grafting: {e.msg}")

proc isNixAvailable(): bool =
  ## Check if Nix is installed and available
  try:
    let (_, exitCode) = execCmdEx("nix --version")
    return exitCode == 0
  except:
    return false

proc getNixPackageInfo(adapter: NixAdapter, packageName: string): NixPackageInfo =
  ## Get package information from nixpkgs
  var info = NixPackageInfo()

  try:
    # Use nix-env to query package information
    let queryCmd = fmt"nix-env -qaA nixpkgs.{packageName} --json"
    let (output, exitCode) = execCmdEx(queryCmd)

    if exitCode == 0 and output.len > 0:
      let jsonData = parseJson(output)

      # Safety check for nil
      if jsonData.isNil:
        return info

      let packageKey = fmt"nixpkgs.{packageName}"

      if jsonData.hasKey(packageKey):
        let pkgData = jsonData[packageKey]
        info.name = packageName
        info.version = pkgData{"version"}.getStr("")

        # Safe access to meta fields
        if not pkgData{"meta"}.isNil:
          info.description = pkgData{"meta"}{"description"}.getStr("")
          info.homepage = pkgData{"meta"}{"homepage"}.getStr("")
        else:
          info.description = ""
          info.homepage = ""

          # Extract license information
          if not pkgData{"meta"}.isNil and pkgData{"meta"}.hasKey("license"):
            let license = pkgData{"meta"}{"license"}
            if not license.isNil and license.kind == JArray:
              for lic in license:
                info.license.add(lic{"spdxId"}.getStr(lic{"shortName"}.getStr("")))
            elif not license.isNil:
              info.license.add(license{"spdxId"}.getStr(license{"shortName"}.getStr("")))

          # Extract maintainer information
          if not pkgData{"meta"}.isNil and pkgData{"meta"}.hasKey("maintainers"):
            for maintainer in pkgData{"meta"}{"maintainers"}:
              info.maintainers.add(maintainer{"name"}.getStr(maintainer{"github"}.getStr("")))

          # Extract platform information
          if not pkgData{"meta"}.isNil and pkgData{"meta"}.hasKey("platforms"):
            for platform in pkgData{"meta"}{"platforms"}:
              info.platforms.add(platform.getStr())

    # If not found with exact name, try searching
    if info.name == "":
      info = searchNixPackage(adapter, packageName)

  except Exception as e:
    echo fmt"Warning: Failed to get Nix package info: {e.msg}"

  info

proc searchNixPackage(adapter: NixAdapter, packageName: string): NixPackageInfo =
  ## Search for a package in nixpkgs
  var info = NixPackageInfo()

  try:
    # Use nix search to find the package
    let searchCmd = fmt"nix search nixpkgs {packageName} --json"
    let (output, exitCode) = execCmdEx(searchCmd)

    if exitCode == 0 and output.len > 0:
      let jsonData = parseJson(output)

      # Find the first matching package
      for key, value in jsonData:
        if key.contains(packageName) or value{"pname"}.getStr("").contains(packageName):
          info.name = value{"pname"}.getStr(packageName)
          info.version = value{"version"}.getStr("")
          info.description = value{"description"}.getStr("")
          break

  except Exception as e:
    echo fmt"Warning: Failed to search Nix packages: {e.msg}"

  info

proc buildNixPackage(adapter: NixAdapter, packageName: string, info: NixPackageInfo): NixBuildResult =
  ## Build a Nix package using nix-build
  result = NixBuildResult(success: false)

  try:
    # Prepare build command
    var buildCmd = fmt"nix-build '<nixpkgs>' -A {packageName}"

    # Add options based on adapter configuration
    if not adapter.allowUnfree:
      buildCmd.add(" --option allow-unfree false")

    if adapter.buildInSandbox:
      buildCmd.add(" --option sandbox true")

    if adapter.useBinaryCache:
      buildCmd.add(" --option substitute true")

    # Add output directory
    buildCmd.add(fmt" --out-link {adapter.cacheDir}/nix-result-{packageName}")

    echo fmt"🔨 Building Nix package: {buildCmd}"
    echo ""

    # Execute build command with streaming output
    let exitCode = execCmd(buildCmd)

    # Read the result from the symlink
    var output = ""
    if exitCode == 0:
      # Parse store path from output
      let lines = output.splitLines()
      for line in lines:
        if line.startsWith("/nix/store/"):
          result.storePath = line.strip()
          break

      if result.storePath == "":
        # Try to read from symlink
        let symlinkPath = fmt"{adapter.cacheDir}/nix-result-{packageName}"
        if symlinkExists(symlinkPath):
          result.storePath = expandSymlink(symlinkPath)

      if result.storePath != "":
        result.success = true
        result.buildLog = output
        echo fmt"✅ Nix build successful: {result.storePath}"
      else:
        result.error = "Could not determine store path from build output"
    else:
      result.error = fmt"Nix build failed with exit code {exitCode}: {output}"

  except Exception as e:
    result.error = fmt"Exception during Nix build: {e.msg}"

proc extractFromNixStore(adapter: NixAdapter, storePath: string, packageName: string, cache: GraftingCache): NixExtractResult =
  ## Extract package from Nix store to GoboLinux structure
  result = NixExtractResult(success: false)

  try:
    if not dirExists(storePath):
      result.error = fmt"Nix store path does not exist: {storePath}"
      return result

    # Create target directory in GoboLinux structure
    let targetDir = cache.cacheDir / "extracted" / packageName
    if dirExists(targetDir):
      removeDir(targetDir)
    createDir(targetDir)

    # Copy files from Nix store to target directory
    let copyCmd = fmt"cp -r {storePath}/* {targetDir}/"
    let (output, exitCode) = execCmdEx(copyCmd)

    if exitCode == 0:
      # Make files writable (Nix store files are read-only)
      let chmodCmd = fmt"chmod -R u+w {targetDir}"
      discard execCmdEx(chmodCmd)

      result.success = true
      result.extractedPath = targetDir
      result.conversionLog = fmt"Extracted from Nix store {storePath} to {targetDir}"
      echo fmt"📦 Extracted Nix package to: {targetDir}"
    else:
      result.error = fmt"Failed to copy from Nix store: {output}"

  except Exception as e:
    result.error = fmt"Exception during extraction: {e.msg}"

proc calculateNixStoreHash(storePath: string): string =
  ## Calculate hash of Nix store path
  try:
    # Extract the hash from the Nix store path
    let pathParts = storePath.split("/")
    if pathParts.len > 3 and pathParts[1] == "nix" and pathParts[2] == "store":
      let storeHash = pathParts[3].split("-")[0]
      return "nix-" & storeHash
    else:
      # Fallback: calculate hash of directory contents
      let hashCmd = fmt"find {storePath} -type f -exec sha256sum {{}} + | sha256sum"
      let (output, exitCode) = execCmdEx(hashCmd)
      if exitCode == 0:
        return "nix-" & output.split()[0]
  except:
    discard

  "nix-hash-error"

method validatePackage*(adapter: NixAdapter, packageName: string): Result[bool, string] =
  ## Validate that a package exists in nixpkgs
  try:
    if not isNixAvailable():
      return Result[bool, string](isOk: false, errValue: "Nix is not installed. Install Nix from https://nixos.org/download.html")

    let info = getNixPackageInfo(adapter, packageName)

    if info.name == "":
      return Result[bool, string](isOk: false, errValue: fmt"Package '{packageName}' not found in nixpkgs")

    return Result[bool, string](isOk: true, okValue: true)

  except JsonParsingError as e:
    return Result[bool, string](isOk: false, errValue: fmt"Failed to parse Nix output: {e.msg}")
  except Exception as e:
    return Result[bool, string](isOk: false, errValue: fmt"Validation error: {e.msg}")

method getPackageInfo*(adapter: NixAdapter, packageName: string): Result[JsonNode, string] =
  ## Get detailed package information from nixpkgs
  try:
    let info = getNixPackageInfo(adapter, packageName)

    if info.name == "":
      return Result[JsonNode, string](isOk: false, errValue: fmt"Package '{packageName}' not found in nixpkgs")

    let jsonResult = %*{
      "name": info.name,
      "version": info.version,
      "description": info.description,
      "homepage": info.homepage,
      "license": info.license,
      "maintainers": info.maintainers,
      "platforms": info.platforms,
      "source": "nixpkgs-" & adapter.nixpkgsChannel,
      "adapter": adapter.name
    }

    return Result[JsonNode, string](isOk: true, okValue: jsonResult)

  except Exception as e:
    return Result[JsonNode, string](isOk: false, errValue: fmt"Error getting package info: {e.msg}")

# Utility functions for Nix integration
proc getNixSystemInfo*(): JsonNode =
  ## Get information about the Nix installation
  var info = %*{
    "available": false,
    "version": "",
    "store_path": "/nix/store",
    "channels": []
  }

  try:
    if isNixAvailable():
      info["available"] = %true

      # Get Nix version
      let (versionOutput, _) = execCmdEx("nix --version")
      info["version"] = %versionOutput.strip()

      # Get channels
      let (channelsOutput, channelsExit) = execCmdEx("nix-channel --list")
      if channelsExit == 0:
        var channels: seq[JsonNode] = @[]
        for line in channelsOutput.splitLines():
          if line.len > 0:
            let parts = line.split()
            if parts.len >= 2:
              channels.add(%*{"name": parts[0], "url": parts[1]})
        info["channels"] = %channels
  except:
    discard

  info

proc listNixPackages*(adapter: NixAdapter, pattern: string = ""): seq[string] =
  ## List available packages in nixpkgs
  result = @[]

  try:
    var searchCmd = "nix-env -qaA nixpkgs"
    if pattern != "":
      searchCmd.add(fmt" | grep -i {pattern}")

    let (output, exitCode) = execCmdEx(searchCmd)
    if exitCode == 0:
      for line in output.splitLines():
        if line.len > 0:
          let parts = line.split()
          if parts.len > 0:
            result.add(parts[0].replace("nixpkgs.", ""))
  except:
    discard