# nimpak/adapters/pkgsrc.nim
# PKGSRC grafting adapter for NetBSD package system

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

type
  PKGSRCAdapter* = ref object of PackageAdapter
    pkgsrcPath*: string
    binaryPackageUrl*: string
    cacheDir*: string
    useBinaryPackages*: bool
    buildFromSource*: bool
    makeFlags*: seq[string]
    pkgDbPath*: string

  PKGSRCPackageInfo* = object
    name*: string
    version*: string
    category*: string
    description*: string
    homepage*: string
    maintainer*: string
    license*: string
    depends*: seq[string]
    conflicts*: seq[string]
    pkgPath*: string
    binaryUrl*: string

  PKGSRCMakefile* = object
    distname*: string
    pkgname*: string
    categories*: seq[string]
    maintainer*: string
    homepage*: string
    comment*: string
    license*: string
    depends*: seq[string]
    buildDepends*: seq[string]
    conflicts*: seq[string]

# Forward declarations
proc findPKGSRCPackage(adapter: PKGSRCAdapter, packageName: string): PKGSRCPackageInfo
proc searchPKGSRCExact(adapter: PKGSRCAdapter, packageName: string): PKGSRCPackageInfo
proc searchPKGSRCFuzzy(adapter: PKGSRCAdapter, packageName: string): PKGSRCPackageInfo
proc searchPKGSRCOnline(adapter: PKGSRCAdapter, packageName: string): PKGSRCPackageInfo
proc parsePKGSRCMakefile(makefilePath: string, category: string, packageName: string): PKGSRCPackageInfo
proc getPKGSRCOnlineDetails(adapter: PKGSRCAdapter, category: string, packageName: string): PKGSRCPackageInfo
proc calculateFileHash(filePath: string): string
proc calculateDirectoryHash(dirPath: string): string
proc graftBinaryPackage(adapter: PKGSRCAdapter, info: PKGSRCPackageInfo, cache: GraftingCache): GraftResult
proc graftFromSource(adapter: PKGSRCAdapter, info: PKGSRCPackageInfo, cache: GraftingCache): GraftResult

proc newPKGSRCAdapter*(config: JsonNode = nil): PKGSRCAdapter =
  ## Create a new PKGSRC adapter with configuration
  result = PKGSRCAdapter(
    name: "pkgsrc",
    priority: 25,
    enabled: true,
    pkgsrcPath: "/usr/pkgsrc",
    binaryPackageUrl: "https://cdn.netbsd.org/pub/pkgsrc/packages/NetBSD",
    cacheDir: "/var/cache/nip/pkgsrc",
    useBinaryPackages: true,
    buildFromSource: false,
    makeFlags: @[],
    pkgDbPath: "/var/db/pkg"
  )

  # Apply configuration if provided
  if config != nil:
    if config.hasKey("pkgsrc_path"):
      result.pkgsrcPath = config["pkgsrc_path"].getStr()
    if config.hasKey("binary_package_url"):
      result.binaryPackageUrl = config["binary_package_url"].getStr()
    if config.hasKey("cache_dir"):
      result.cacheDir = config["cache_dir"].getStr()
    if config.hasKey("use_binary_packages"):
      result.useBinaryPackages = config["use_binary_packages"].getBool()
    if config.hasKey("build_from_source"):
      result.buildFromSource = config["build_from_source"].getBool()
    if config.hasKey("make_flags"):
      result.makeFlags = @[]
      for flag in config["make_flags"]:
        result.makeFlags.add(flag.getStr())

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

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

  try:
    # First, find the package in PKGSRC
    let packageInfo = findPKGSRCPackage(adapter, packageName)
    if packageInfo.name == "":
      result.errors.add(fmt"Package '{packageName}' not found in PKGSRC")
      return result

    # Try binary package first if enabled
    if adapter.useBinaryPackages:
      echo "🔍 Trying binary package..."
      let binaryResult = graftBinaryPackage(adapter, packageInfo, cache)
      if binaryResult.success:
        return binaryResult

    # Fall back to building from source if enabled
    if adapter.buildFromSource:
      echo "🔨 Building from source..."
      let sourceResult = graftFromSource(adapter, packageInfo, cache)
      if sourceResult.success:
        return sourceResult

    result.errors.add("Neither binary package nor source build succeeded")

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

  result

proc findPKGSRCPackage(adapter: PKGSRCAdapter, packageName: string): PKGSRCPackageInfo =
  ## Find a package in the PKGSRC tree
  var info = PKGSRCPackageInfo()

  try:
    # First try to find by exact name
    let exactResult = searchPKGSRCExact(adapter, packageName)
    if exactResult.name != "":
      return exactResult

    # Try fuzzy search
    let fuzzyResult = searchPKGSRCFuzzy(adapter, packageName)
    if fuzzyResult.name != "":
      return fuzzyResult

    # Try online package database
    let onlineResult = searchPKGSRCOnline(adapter, packageName)
    if onlineResult.name != "":
      return onlineResult

  except Exception as e:
    echo fmt"Warning: Error searching PKGSRC: {e.msg}"

  info

proc searchPKGSRCExact(adapter: PKGSRCAdapter, packageName: string): PKGSRCPackageInfo =
  ## Search for exact package name in local PKGSRC tree
  var info = PKGSRCPackageInfo()

  try:
    if not dirExists(adapter.pkgsrcPath):
      return info

    # Search through categories
    for category in walkDirs(adapter.pkgsrcPath / "*"):
      let categoryName = extractFilename(category)
      if categoryName in ["CVS", "distfiles", "packages", "bootstrap"]:
        continue

      let packageDir = category / packageName
      if dirExists(packageDir):
        let makefilePath = packageDir / "Makefile"
        if fileExists(makefilePath):
          info = parsePKGSRCMakefile(makefilePath, categoryName, packageName)
          if info.name != "":
            return info

  except Exception as e:
    echo fmt"Warning: Error in exact PKGSRC search: {e.msg}"

  info

proc searchPKGSRCFuzzy(adapter: PKGSRCAdapter, packageName: string): PKGSRCPackageInfo =
  ## Fuzzy search for package name in PKGSRC
  var info = PKGSRCPackageInfo()

  try:
    if not dirExists(adapter.pkgsrcPath):
      return info

    # Use find command for fuzzy search
    let findCmd = fmt"find {adapter.pkgsrcPath} -name '*{packageName}*' -type d -maxdepth 2"
    let (output, exitCode) = execCmdEx(findCmd)

    if exitCode == 0:
      for line in output.splitLines():
        if line.len > 0 and line.contains("/"):
          let parts = line.split("/")
          if parts.len >= 2:
            let category = parts[^2]
            let pkgName = parts[^1]
            let makefilePath = line / "Makefile"

            if fileExists(makefilePath):
              info = parsePKGSRCMakefile(makefilePath, category, pkgName)
              if info.name != "":
                return info

  except Exception as e:
    echo fmt"Warning: Error in fuzzy PKGSRC search: {e.msg}"

  info

proc searchPKGSRCOnline(adapter: PKGSRCAdapter, packageName: string): PKGSRCPackageInfo =
  ## Search for package in online PKGSRC database
  var info = PKGSRCPackageInfo()

  try:
    # Query the NetBSD package database
    let searchUrl = fmt"https://pkgsrc.se/search?q={packageName}"
    let curlCmd = "curl -s '" & searchUrl & "' | grep -o 'href=\"/[^/]*/[^\"]*\"' | head -1"
    let (output, exitCode) = execCmdEx(curlCmd)

    if exitCode == 0 and output.len > 0:
      # Parse the result to extract category and package name
      let href = output.strip()
      if href.startsWith("href=\"/") and href.endsWith("\""):
        let path = href[7..^2]  # Remove href="/ and "
        let parts = path.split("/")
        if parts.len == 2:
          info.category = parts[0]
          info.name = parts[1]
          info.pkgPath = fmt"{info.category}/{info.name}"

          # Try to get more details
          let detailsResult = getPKGSRCOnlineDetails(adapter, info.category, info.name)
          if detailsResult.description != "":
            info = detailsResult

  except Exception as e:
    echo fmt"Warning: Error in online PKGSRC search: {e.msg}"

  info

proc getPKGSRCOnlineDetails(adapter: PKGSRCAdapter, category: string, packageName: string): PKGSRCPackageInfo =
  ## Get detailed package information from online PKGSRC database
  var info = PKGSRCPackageInfo(
    name: packageName,
    category: category,
    pkgPath: fmt"{category}/{packageName}"
  )

  try:
    let detailUrl = fmt"https://pkgsrc.se/{category}/{packageName}"
    let curlCmd = fmt"curl -s '{detailUrl}'"
    let (output, exitCode) = execCmdEx(curlCmd)

    if exitCode == 0:
      # Parse HTML to extract package information
      for line in output.splitLines():
        if "Description:" in line:
          # Extract description from HTML
          let start = line.find(">") + 1
          let endTag = line.find("</", start)
          if start > 0 and endTag > start:
            info.description = line[start..<endTag].strip()
        elif "Homepage:" in line and "href=" in line:
          # Extract homepage URL
          let hrefStart = line.find("href=\"") + 6
          let hrefEnd = line.find("\"", hrefStart)
          if hrefStart > 5 and hrefEnd > hrefStart:
            info.homepage = line[hrefStart..<hrefEnd]

  except Exception as e:
    echo fmt"Warning: Error getting PKGSRC online details: {e.msg}"

  info

proc parsePKGSRCMakefile(makefilePath: string, category: string, packageName: string): PKGSRCPackageInfo =
  ## Parse a PKGSRC Makefile to extract package information
  var info = PKGSRCPackageInfo(
    name: packageName,
    category: category,
    pkgPath: fmt"{category}/{packageName}"
  )

  try:
    let content = readFile(makefilePath)

    for line in content.splitLines():
      let trimmed = line.strip()
      if trimmed.startsWith("#") or trimmed.len == 0:
        continue

      if trimmed.startsWith("DISTNAME="):
        let distname = trimmed[9..^1].strip()
        # Extract version from distname
        let parts = distname.split("-")
        if parts.len > 1:
          info.version = parts[^1]
      elif trimmed.startsWith("PKGNAME="):
        let pkgname = trimmed[8..^1].strip()
        if "-" in pkgname:
          let parts = pkgname.split("-")
          if parts.len > 1:
            info.version = parts[^1]
      elif trimmed.startsWith("COMMENT="):
        info.description = trimmed[8..^1].strip()
      elif trimmed.startsWith("HOMEPAGE="):
        info.homepage = trimmed[9..^1].strip()
      elif trimmed.startsWith("MAINTAINER="):
        info.maintainer = trimmed[11..^1].strip()
      elif trimmed.startsWith("LICENSE="):
        info.license = trimmed[8..^1].strip()
      elif trimmed.startsWith("DEPENDS+="):
        let dep = trimmed[9..^1].strip()
        info.depends.add(dep)
      elif trimmed.startsWith("CONFLICTS+="):
        let conflict = trimmed[11..^1].strip()
        info.conflicts.add(conflict)

  except Exception as e:
    echo fmt"Warning: Error parsing PKGSRC Makefile: {e.msg}"

  info

proc graftBinaryPackage(adapter: PKGSRCAdapter, info: PKGSRCPackageInfo, cache: GraftingCache): GraftResult =
  ## Graft a binary package from PKGSRC
  var result = GraftResult(success: false, errors: @[])

  try:
    # Construct binary package URL
    let arch = "x86_64"  # TODO: Detect actual architecture
    let osVersion = "9.0"  # TODO: Detect NetBSD version or use generic
    let binaryUrl = fmt"{adapter.binaryPackageUrl}/{arch}/{osVersion}/All/{info.name}-{info.version}.tgz"

    echo fmt"📦 Downloading binary package: {binaryUrl}"

    # Download binary package
    let packageFile = adapter.cacheDir / fmt"{info.name}-{info.version}.tgz"
    createDir(adapter.cacheDir)

    let downloadCmd = fmt"curl -L -o {packageFile} {binaryUrl}"
    let (downloadOutput, downloadExit) = execCmdEx(downloadCmd)

    if downloadExit != 0:
      result.errors.add(fmt"Failed to download binary package: {downloadOutput}")
      return result

    if not fileExists(packageFile):
      result.errors.add("Binary package file not found after download")
      return result

    # Extract binary package
    let extractDir = adapter.cacheDir / "extracted" / info.name
    if dirExists(extractDir):
      removeDir(extractDir)
    createDir(extractDir)

    let extractCmd = fmt"tar -xzf {packageFile} -C {extractDir}"
    let (extractOutput, extractExit) = execCmdEx(extractCmd)

    if extractExit != 0:
      result.errors.add(fmt"Failed to extract binary package: {extractOutput}")
      return result

    # Calculate hashes
    let originalHash = calculateFileHash(packageFile)
    let graftHash = calculateGraftHash(info.name, "pkgsrc", now())

    # Create metadata
    let metadata = GraftedPackageMetadata(
      packageName: info.name,
      version: info.version,
      source: "pkgsrc-binary",
      graftedAt: now(),
      originalHash: originalHash,
      graftHash: graftHash,
      buildLog: fmt"Downloaded binary package from {binaryUrl}",
      provenance: ProvenanceInfo(
        originalSource: "pkgsrc-binary",
        downloadUrl: binaryUrl,
        archivePath: packageFile,
        extractedPath: extractDir,
        conversionLog: fmt"Extracted PKGSRC binary package to {extractDir}"
      )
    )

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

    echo fmt"✅ Successfully grafted PKGSRC binary package: {info.name} {info.version}"

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

  result

proc graftFromSource(adapter: PKGSRCAdapter, info: PKGSRCPackageInfo, cache: GraftingCache): GraftResult =
  ## Build and graft a package from PKGSRC source
  var result = GraftResult(success: false, errors: @[])

  try:
    if not dirExists(adapter.pkgsrcPath):
      result.errors.add(fmt"PKGSRC tree not found at {adapter.pkgsrcPath}")
      return result

    let packageDir = adapter.pkgsrcPath / info.pkgPath
    if not dirExists(packageDir):
      result.errors.add(fmt"Package directory not found: {packageDir}")
      return result

    echo fmt"🔨 Building PKGSRC package from source: {packageDir}"

    # Build the package using bmake
    var buildCmd = fmt"cd {packageDir} && bmake"
    for flag in adapter.makeFlags:
      buildCmd.add(fmt" {flag}")

    let (buildOutput, buildExit) = execCmdEx(buildCmd)

    if buildExit != 0:
      result.errors.add(fmt"PKGSRC build failed: {buildOutput}")
      return result

    # Install to temporary directory
    let installDir = adapter.cacheDir / "built" / info.name
    if dirExists(installDir):
      removeDir(installDir)
    createDir(installDir)

    let installCmd = fmt"cd {packageDir} && bmake DESTDIR={installDir} install"
    let (installOutput, installExit) = execCmdEx(installCmd)

    if installExit != 0:
      result.errors.add(fmt"PKGSRC install failed: {installOutput}")
      return result

    # Calculate hashes
    let sourceHash = calculateDirectoryHash(packageDir)
    let graftHash = calculateGraftHash(info.name, "pkgsrc", now())

    # Create metadata
    let metadata = GraftedPackageMetadata(
      packageName: info.name,
      version: info.version,
      source: "pkgsrc-source",
      graftedAt: now(),
      originalHash: sourceHash,
      graftHash: graftHash,
      buildLog: buildOutput & "\n" & installOutput,
      provenance: ProvenanceInfo(
        originalSource: "pkgsrc-source",
        downloadUrl: fmt"https://github.com/NetBSD/pkgsrc/tree/trunk/{info.pkgPath}",
        archivePath: packageDir,
        extractedPath: installDir,
        conversionLog: fmt"Built from PKGSRC source and installed to {installDir}"
      )
    )

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

    echo fmt"✅ Successfully built PKGSRC package from source: {info.name} {info.version}"

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

  result

proc calculateFileHash(filePath: string): string =
  ## Calculate hash of a file
  try:
    let hashCmd = fmt"sha256sum {filePath}"
    let (output, exitCode) = execCmdEx(hashCmd)
    if exitCode == 0:
      return "pkgsrc-" & output.split()[0]
  except:
    discard
  "pkgsrc-hash-error"

proc calculateDirectoryHash(dirPath: string): string =
  ## Calculate hash of directory contents
  try:
    let hashCmd = fmt"find {dirPath} -type f -exec sha256sum {{}} + | sha256sum"
    let (output, exitCode) = execCmdEx(hashCmd)
    if exitCode == 0:
      return "pkgsrc-src-" & output.split()[0]
  except:
    discard
  "pkgsrc-src-hash-error"

method validatePackage*(adapter: PKGSRCAdapter, packageName: string): Result[bool, string] =
  ## Validate that a package exists in PKGSRC
  try:
    let info = findPKGSRCPackage(adapter, packageName)
    return Result[bool, string](isOk: true, okValue: info.name != "")
  except Exception as e:
    return Result[bool, string](isOk: false, errValue: fmt"Validation error: {e.msg}")

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

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

    let result = %*{
      "name": info.name,
      "version": info.version,
      "category": info.category,
      "description": info.description,
      "homepage": info.homepage,
      "maintainer": info.maintainer,
      "license": info.license,
      "depends": info.depends,
      "conflicts": info.conflicts,
      "pkg_path": info.pkgPath,
      "source": "pkgsrc",
      "adapter": adapter.name
    }

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

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

# Utility functions
proc isPKGSRCAvailable*(adapter: PKGSRCAdapter): bool =
  ## Check if PKGSRC is available on the system
  dirExists(adapter.pkgsrcPath) or findExe("bmake") != ""

proc commandExists(command: string): bool =
  ## Check if a command exists in PATH
  try:
    let (_, exitCode) = execCmdEx(fmt"which {command}")
    return exitCode == 0
  except:
    return false

proc listPKGSRCCategories*(adapter: PKGSRCAdapter): seq[string] =
  ## List available PKGSRC categories
  result = @[]

  try:
    if dirExists(adapter.pkgsrcPath):
      for category in walkDirs(adapter.pkgsrcPath / "*"):
        let categoryName = extractFilename(category)
        if categoryName notin ["CVS", "distfiles", "packages", "bootstrap"]:
          result.add(categoryName)
  except:
    discard

proc listPKGSRCPackages*(adapter: PKGSRCAdapter, category: string = ""): seq[string] =
  ## List packages in PKGSRC (optionally filtered by category)
  result = @[]

  try:
    if not dirExists(adapter.pkgsrcPath):
      return result

    if category != "":
      let categoryDir = adapter.pkgsrcPath / category
      if dirExists(categoryDir):
        for pkg in walkDirs(categoryDir / "*"):
          let pkgName = extractFilename(pkg)
          if pkgName != "CVS":
            result.add(pkgName)
    else:
      for cat in listPKGSRCCategories(adapter):
        for pkg in listPKGSRCPackages(adapter, cat):
          result.add(fmt"{cat}/{pkg}")
  except:
    discard