## Pacman Database Adapter for NIP
##
## This module provides integration with the existing pacman package manager,
## allowing NIP to read, understand, and manage pacman-installed packages.
## This enables gradual migration from pacman to NIP on Arch Linux systems.

import std/[os, strutils, tables, times, sequtils, options, strformat, hashes, osproc]
from ../cas import VoidResult, Result, ok, get, err
import ../grafting

type
  PacmanPackage* = object
    name*: string
    version*: string
    description*: string
    architecture*: string
    url*: string
    licenses*: seq[string]
    groups*: seq[string]
    provides*: seq[string]
    depends*: seq[string]
    optdepends*: seq[string]
    conflicts*: seq[string]
    replaces*: seq[string]
    installDate*: DateTime
    installReason*: string
    installScript*: bool
    packager*: string
    buildDate*: DateTime
    installSize*: int64
    files*: seq[string]

  PacmanDatabase* = object
    packages*: Table[string, PacmanPackage]
    dbPath*: string

  PacmanAdapter* = object
    database*: PacmanDatabase
    nipDatabase*: string  # Path to NIP database for integration

const
  DEFAULT_PACMAN_DB* = "/var/lib/pacman/local"
  PACMAN_DESC_FILE* = "desc"
  PACMAN_FILES_FILE* = "files"

proc initPacmanAdapter*(pacmanDbPath: string = DEFAULT_PACMAN_DB,
                       nipDbPath: string = ""): PacmanAdapter =
  ## Initialize the pacman adapter with database paths
  result.database.dbPath = pacmanDbPath
  result.database.packages = initTable[string, PacmanPackage]()
  result.nipDatabase = nipDbPath

proc parseDescFile(descPath: string): PacmanPackage =
  ## Parse a pacman package desc file
  result = PacmanPackage()

  if not fileExists(descPath):
    return

  let content = readFile(descPath)
  var currentSection = ""
  var currentValues: seq[string] = @[]

  for line in content.splitLines():
    let trimmedLine = line.strip()

    if trimmedLine.startsWith("%") and trimmedLine.endsWith("%"):
      # Process previous section
      if currentSection != "":
        case currentSection:
        of "NAME":
          if currentValues.len > 0:
            result.name = currentValues[0]
        of "VERSION":
          if currentValues.len > 0:
            result.version = currentValues[0]
        of "DESC":
          if currentValues.len > 0:
            result.description = currentValues.join(" ")
        of "ARCH":
          if currentValues.len > 0:
            result.architecture = currentValues[0]
        of "URL":
          if currentValues.len > 0:
            result.url = currentValues[0]
        of "LICENSE":
          result.licenses = currentValues
        of "GROUPS":
          result.groups = currentValues
        of "PROVIDES":
          result.provides = currentValues
        of "DEPENDS":
          result.depends = currentValues
        of "OPTDEPENDS":
          result.optdepends = currentValues
        of "CONFLICTS":
          result.conflicts = currentValues
        of "REPLACES":
          result.replaces = currentValues
        of "INSTALLDATE":
          if currentValues.len > 0:
            try:
              result.installDate = fromUnix(parseInt(currentValues[0])).utc
            except:
              result.installDate = now()
        of "REASON":
          if currentValues.len > 0:
            result.installReason = currentValues[0]
        of "PACKAGER":
          if currentValues.len > 0:
            result.packager = currentValues[0]
        of "BUILDDATE":
          if currentValues.len > 0:
            try:
              result.buildDate = fromUnix(parseInt(currentValues[0])).utc
            except:
              result.buildDate = now()
        of "SIZE":
          if currentValues.len > 0:
            try:
              result.installSize = parseInt(currentValues[0])
            except:
              result.installSize = 0

      # Start new section
      currentSection = trimmedLine[1..^2]  # Remove % characters
      currentValues = @[]
    elif trimmedLine != "" and currentSection != "":
      currentValues.add(trimmedLine)

  # Process the last section after loop ends
  if currentSection != "":
    case currentSection:
    of "NAME":
      if currentValues.len > 0:
        result.name = currentValues[0]
    of "VERSION":
      if currentValues.len > 0:
        result.version = currentValues[0]
    of "DESC":
      if currentValues.len > 0:
        result.description = currentValues.join(" ")
    of "ARCH":
      if currentValues.len > 0:
        result.architecture = currentValues[0]
    of "URL":
      if currentValues.len > 0:
        result.url = currentValues[0]
    of "LICENSE":
      result.licenses = currentValues
    of "GROUPS":
      result.groups = currentValues
    of "PROVIDES":
      result.provides = currentValues
    of "DEPENDS":
      result.depends = currentValues
    of "OPTDEPENDS":
      result.optdepends = currentValues
    of "CONFLICTS":
      result.conflicts = currentValues
    of "REPLACES":
      result.replaces = currentValues
    of "INSTALLDATE":
      if currentValues.len > 0:
        try:
          result.installDate = fromUnix(parseInt(currentValues[0])).utc
        except:
          result.installDate = now()
    of "REASON":
      if currentValues.len > 0:
        result.installReason = currentValues[0]
    of "PACKAGER":
      if currentValues.len > 0:
        result.packager = currentValues[0]
    of "BUILDDATE":
      if currentValues.len > 0:
        try:
          result.buildDate = fromUnix(parseInt(currentValues[0])).utc
        except:
          result.buildDate = now()
    of "SIZE":
      if currentValues.len > 0:
        try:
          result.installSize = parseInt(currentValues[0])
        except:
          result.installSize = 0
    else:
      discard

proc parseFilesFile(filesPath: string): seq[string] =
  ## Parse a pacman package files file
  result = @[]

  if not fileExists(filesPath):
    return

  let content = readFile(filesPath)
  var inFilesSection = false

  for line in content.splitLines():
    let trimmedLine = line.strip()

    if trimmedLine == "%FILES%":
      inFilesSection = true
      continue
    elif trimmedLine.startsWith("%") and trimmedLine.endsWith("%"):
      inFilesSection = false
      continue

    if inFilesSection and trimmedLine != "":
      result.add(trimmedLine)

proc loadPacmanDatabase*(adapter: var PacmanAdapter): VoidResult[string] =
  ## Load the complete pacman database
  if not dirExists(adapter.database.dbPath):
    return VoidResult[string](isOk: false, errValue: "Pacman database directory not found: " & adapter.database.dbPath)

  var packagesLoaded = 0

  try:
    for kind, path in walkDir(adapter.database.dbPath):
      if kind == pcDir:
        let packageDir = path
        let descFile = packageDir / PACMAN_DESC_FILE
        let filesFile = packageDir / PACMAN_FILES_FILE

        if fileExists(descFile):
          var pkg = parseDescFile(descFile)

          # Load files list
          pkg.files = parseFilesFile(filesFile)

          # Add to database
          if pkg.name != "":
            adapter.database.packages[pkg.name] = pkg
            packagesLoaded.inc

    echo "✅ Loaded ", packagesLoaded, " packages from pacman database"

    # Debug: Check if htop is in the database
    if "htop" in adapter.database.packages:
      echo "DEBUG: htop found in database!"
    else:
      echo "DEBUG: htop NOT in database"
      echo "DEBUG: First 10 package names:"
      var count = 0
      for name in adapter.database.packages.keys:
        echo "  - ", name
        count.inc
        if count >= 10:
          break

    return VoidResult[string](isOk: true)

  except Exception as e:
    return VoidResult[string](isOk: false, errValue: "Failed to load pacman database: " & e.msg)

proc getPackage*(adapter: PacmanAdapter, name: string): Option[PacmanPackage] =
  ## Get a specific package from the pacman database
  if name in adapter.database.packages:
    return some(adapter.database.packages[name])
  else:
    return none(PacmanPackage)

proc listPackages*(adapter: PacmanAdapter): seq[PacmanPackage] =
  ## List all packages in the pacman database
  result = @[]
  for pkg in adapter.database.packages.values:
    result.add(pkg)

proc searchPackages*(adapter: PacmanAdapter, query: string): seq[PacmanPackage] =
  ## Search packages by name or description
  result = @[]
  let lowerQuery = query.toLower()

  for pkg in adapter.database.packages.values:
    if lowerQuery in pkg.name.toLower() or lowerQuery in pkg.description.toLower():
      result.add(pkg)

type
  PackageInfo* = object
    name*: string
    version*: string
    description*: string
    dependencies*: seq[Dependency]
    installSize*: int64
    installDate*: DateTime

  Dependency* = object
    name*: string
    version*: string
    optional*: bool

proc convertToNipPackage*(pacmanPkg: PacmanPackage): PackageInfo =
  ## Convert a pacman package to NIP package format
  result = PackageInfo(
    name: pacmanPkg.name,
    version: pacmanPkg.version,
    description: pacmanPkg.description,
    # Map pacman dependencies to NIP format
    dependencies: pacmanPkg.depends.mapIt(Dependency(
      name: it.split(">=")[0].split("=")[0].strip(),  # Remove version constraints for now
      version: "*",  # TODO: Parse version constraints properly
      optional: false
    )),
    installSize: pacmanPkg.installSize,
    installDate: pacmanPkg.installDate
  )

proc syncWithNip*(adapter: var PacmanAdapter): Result[int, string] =
  ## Sync pacman packages with NIP database
  ## Returns number of packages synced
  var syncedCount = 0

  try:
    for pkg in adapter.database.packages.values:
      let nipPkg = convertToNipPackage(pkg)
      # TODO: Add to NIP database
      # This would integrate with the existing NIP database system
      syncedCount.inc

    return Result[int, string](isOk: true, value: syncedCount)

  except Exception as e:
    return Result[int, string](isOk: false, error: "Failed to sync with NIP: " & e.msg)

proc getPackageInfo*(adapter: PacmanAdapter, name: string): string =
  ## Get detailed package information in human-readable format
  let pkgOpt = adapter.getPackage(name)
  if pkgOpt.isNone:
    return "Package not found: " & name

  let pkg = pkgOpt.get()
  result = "📦 " & pkg.name & " " & pkg.version & "\n"
  result.add("Description: " & pkg.description & "\n")
  result.add("Architecture: " & pkg.architecture & "\n")
  result.add("Install Date: " & $pkg.installDate & "\n")
  result.add("Install Size: " & $(pkg.installSize div 1024) & " KB\n")

  if pkg.depends.len > 0:
    result.add("Dependencies: " & pkg.depends.join(", ") & "\n")

  if pkg.optdepends.len > 0:
    result.add("Optional Dependencies: " & pkg.optdepends.join(", ") & "\n")

  if pkg.files.len > 0:
    result.add("Files: " & $pkg.files.len & " files\n")

proc getDependencyTree*(adapter: PacmanAdapter, packageName: string,
                       visited: var seq[string] = @[]): seq[string] =
  ## Get the dependency tree for a package (recursive)
  result = @[]

  if packageName in visited:
    return  # Avoid circular dependencies

  visited.add(packageName)

  let pkgOpt = adapter.getPackage(packageName)
  if pkgOpt.isNone:
    return

  let pkg = pkgOpt.get()
  for dep in pkg.depends:
    let depName = dep.split(">=")[0].split("=")[0].strip()
    result.add(depName)

    # Recursively get dependencies
    let subDeps = adapter.getDependencyTree(depName, visited)
    for subDep in subDeps:
      if subDep notin result:
        result.add(subDep)

proc getSystemStats*(adapter: PacmanAdapter): tuple[totalPackages: int, totalSize: int64] =
  ## Get system statistics from pacman database
  result.totalPackages = adapter.database.packages.len
  result.totalSize = 0

  for pkg in adapter.database.packages.values:
    result.totalSize += pkg.installSize

# CLI Integration functions for NIP commands

proc nipPacmanSync*(): Result[string, string] =
  ## NIP command: nip pacman-sync
  ## Synchronize NIP with existing pacman installation
  var adapter = initPacmanAdapter()

  echo "🔄 Synchronizing with pacman database..."

  let loadResult = adapter.loadPacmanDatabase()
  if not loadResult.isOk:
    return Result[string, string](isOk: false, error: loadResult.errValue)

  let syncResult = adapter.syncWithNip()
  if not syncResult.isOk:
    return Result[string, string](isOk: false, error: syncResult.error)

  let stats = adapter.getSystemStats()
  let message = "✅ Synchronized " & $syncResult.get() & " packages\n" &
                "📊 Total: " & $stats.totalPackages & " packages, " &
                $(stats.totalSize div (1024*1024)) & " MB"

  return Result[string, string](isOk: true, value: message)

proc nipPacmanList*(query: string = ""): Result[string, string] =
  ## NIP command: nip pacman-list [query]
  ## List pacman packages, optionally filtered by query
  var adapter = initPacmanAdapter()

  let loadResult = adapter.loadPacmanDatabase()
  if not loadResult.isOk:
    return Result[string, string](isOk: false, error: loadResult.errValue)

  let packages = if query == "":
    adapter.listPackages()
  else:
    adapter.searchPackages(query)

  var result = "📦 Pacman Packages"
  if query != "":
    result.add(" (matching '" & query & "')")
  result.add(":\n\n")

  for pkg in packages:
    result.add("• " & pkg.name & " " & pkg.version)
    if pkg.description != "":
      result.add(" - " & pkg.description)
    result.add("\n")

  result.add("\nTotal: " & $packages.len & " packages")
  return Result[string, string](isOk: true, value: result)

proc nipPacmanInfo*(packageName: string): Result[string, string] =
  ## NIP command: nip pacman-info <package>
  ## Show detailed information about a pacman package
  var adapter = initPacmanAdapter()

  let loadResult = adapter.loadPacmanDatabase()
  if not loadResult.isOk:
    return Result[string, string](isOk: false, error: loadResult.errValue)

  let info = adapter.getPackageInfo(packageName)
  return Result[string, string](isOk: true, value: info)

proc nipPacmanDeps*(packageName: string): Result[string, string] =
  ## NIP command: nip pacman-deps <package>
  ## Show dependency tree for a pacman package
  var adapter = initPacmanAdapter()

  let loadResult = adapter.loadPacmanDatabase()
  if not loadResult.isOk:
    return Result[string, string](isOk: false, error: loadResult.errValue)

  var visited: seq[string] = @[]
  let deps = adapter.getDependencyTree(packageName, visited)

  var result = "🌳 Dependency tree for " & packageName & ":\n\n"
  for i, dep in deps:
    let prefix = if i == deps.len - 1: "└── " else: "├── "
    result.add(prefix & dep & "\n")

  if deps.len == 0:
    result.add("No dependencies found.\n")
  else:
    result.add("\nTotal dependencies: " & $deps.len)

  return Result[string, string](isOk: true, value: result)

# Grafting adapter methods for coordinator integration

method validatePackage*(adapter: PacmanAdapter, packageName: string): Result[bool, string] =
  ## Validate if a package exists using pacman -Ss (checks repos)
  try:
    # Use pacman to search for package (checks both local and remote)
    let (output, exitCode) = execCmdEx(fmt"pacman -Ss '^{packageName}$'")

    if exitCode == 0 and output.len > 0:
      return Result[bool, string](isOk: true, value: true)
    else:
      return Result[bool, string](isOk: true, value: false)

  except Exception as e:
    return Result[bool, string](isOk: false, error: "Failed to validate package: " & e.msg)

proc isPackageInstalled(adapter: PacmanAdapter, packageName: string): bool =
  ## Check if package is installed locally using pacman -Q
  try:
    let (_, exitCode) = execCmdEx(fmt"pacman -Q {packageName}")
    return exitCode == 0
  except:
    return false

method graftPackage*(adapter: var PacmanAdapter, packageName: string, cache: GraftingCache): GraftResult =
  ## Graft a package from Pacman (local or remote)
  echo fmt"🌱 Grafting package from Pacman: {packageName}"

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

  try:
    # 1. Check if package is installed locally
    let isInstalled = adapter.isPackageInstalled(packageName)

    var pkg: PacmanPackage
    var version: string

    if isInstalled:
      echo "📦 Package is installed locally, using local installation..."

      # Load database to get package info
      if adapter.database.packages.len == 0:
        let loadResult = adapter.loadPacmanDatabase()
        if not loadResult.isOk:
          result.errors.add("Failed to load Pacman database: " & loadResult.errValue)
          return result

      # Get package from database
      let pkgOpt = adapter.getPackage(packageName)
      if pkgOpt.isNone:
        result.errors.add(fmt"Package '{packageName}' installed but not in database")
        return result

      pkg = pkgOpt.get()
      version = pkg.version
    else:
      echo "📥 Package not installed, will use pacman to query..."

      # Get version from pacman -Si
      let (infoOutput, infoExit) = execCmdEx(fmt"pacman -Si {packageName}")
      if infoExit != 0:
        result.errors.add(fmt"Package '{packageName}' not found in Pacman repos")
        return result

      # Parse version from output
      for line in infoOutput.splitLines():
        if line.startsWith("Version"):
          version = line.split(":")[1].strip()
          break

      if version == "":
        result.errors.add("Could not determine package version")
        return result

    # 2. Create extraction directory
    let extractDir = cache.cacheDir / packageName & "-" & version
    createDir(extractDir)

    # 3. Extract files (different approach for installed vs not installed)
    var copiedFiles = 0

    if isInstalled:
      # Copy files from local installation using cp -a to preserve permissions
      echo fmt"📂 Copying {pkg.files.len} files from local installation..."

      # Use cp -a (archive mode) to preserve permissions, ownership, timestamps
      # This is critical for executables to maintain their execute bits
      for filePath in pkg.files:
        let srcPath = "/" & filePath
        if fileExists(srcPath):
          let destPath = extractDir / filePath
          let destDir = destPath.parentDir()
          createDir(destDir)

          # Use cp -a to preserve all attributes including execute permissions
          let copyCmd = fmt"cp -a {srcPath} {destPath}"
          let (output, exitCode) = execCmdEx(copyCmd)
          if exitCode == 0:
            copiedFiles.inc
          # Skip files we can't copy (permissions, etc.)

      if copiedFiles == 0:
        result.errors.add("No files could be copied from package")
        return result

      echo fmt"✅ Copied {copiedFiles} files (permissions preserved)"
    else:
      # For now, return error - download support coming next
      result.errors.add("Package not installed. Download support coming soon!")
      result.errors.add("Please install with: sudo pacman -S " & packageName)
      return result

    # Calculate hashes
    let originalHash = "blake2b-" & $hash(pkg.name & pkg.version)
    let graftHash = calculateGraftHash(packageName, "pacman", now())

    # Create metadata
    let metadata = GraftedPackageMetadata(
      packageName: pkg.name,
      version: pkg.version,
      source: "pacman",
      graftedAt: now(),
      originalHash: originalHash,
      graftHash: graftHash,
      buildLog: fmt"Grafted from Pacman database\nInstalled: {pkg.installDate}\nSize: {pkg.installSize} bytes",
      provenance: ProvenanceInfo(
        originalSource: "pacman-" & pkg.architecture,
        downloadUrl: pkg.url,
        archivePath: adapter.database.dbPath / (pkg.name & "-" & pkg.version),
        extractedPath: extractDir,
        conversionLog: fmt"Copied {copiedFiles} files from Pacman installation"
      )
    )

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

    echo fmt"✅ Successfully grafted from Pacman: {pkg.name} {pkg.version}"
    echo fmt"📍 Extracted to: {extractDir}"
    echo fmt"📊 Copied {copiedFiles} files"

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