## variant_database.nim
## Database operations for variant management
## Extends the package database with variant tracking

import std/[os, json, tables, times, strformat, options, strutils, sequtils]
import variant_types

type
  VariantDatabase* = ref object
    dbPath*: string
    variants*: Table[string, VariantRecord]  # fingerprint -> record
    references*: Table[string, seq[string]]   # variant fingerprint -> list of dependent package names (Task 14.2)

  # DEPRECATED: Use Option[VariantRecord] instead
  VariantQueryResult* {.deprecated: "Use Option[VariantRecord] instead".} = object
    found*: bool
    record*: VariantRecord

  VariantReferenceInfo* = object
    ## Information about variant references (Task 14.2)
    fingerprint*: string
    referencedBy*: seq[string]  # List of packages that depend on this variant
    canDelete*: bool

const
  VARIANTS_DB = "variants.json"

# Forward declarations
proc getVariantReferences*(db: VariantDatabase, variantFingerprint: string): seq[string]

# #############################################################################
# Database Initialization
# #############################################################################

proc newVariantDatabase*(dbPath: string): VariantDatabase =
  ## Create a new variant database instance
  result = VariantDatabase(
    dbPath: dbPath,
    variants: initTable[string, VariantRecord](),
    references: initTable[string, seq[string]]()  # Task 14.2
  )

  # Ensure database directory exists
  let expandedPath = if dbPath.startsWith("~"): expandTilde(dbPath) else: dbPath
  createDir(expandedPath)
  result.dbPath = expandedPath

# #############################################################################
# Persistence Operations
# #############################################################################

proc saveVariants*(db: VariantDatabase) =
  ## Save variants database to disk atomically with error handling
  var data = newJObject()

  for fingerprint, variant in db.variants.pairs:
    # Convert domains table to JSON
    var domainsJson = newJObject()
    for domain, values in variant.domains:
      domainsJson[domain] = %values

    # Convert toolchain to JSON
    var toolchainJson = %*{
      "name": variant.toolchain.name,
      "version": variant.toolchain.version
    }

    # Convert target to JSON
    var targetJson = %*{
      "arch": variant.target.arch,
      "os": variant.target.os
    }

    data[fingerprint] = %*{
      "fingerprint": variant.fingerprint,
      "packageName": variant.packageName,
      "version": variant.version,
      "domains": domainsJson,
      "installPath": variant.installPath,
      "installedAt": variant.installedAt.toUnix(),
      "toolchain": toolchainJson,
      "target": targetJson
    }

  # Save references (Task 14.2)
  var referencesJson = newJObject()
  for fingerprint, refs in db.references.pairs:
    referencesJson[fingerprint] = %refs

  # Create combined data structure
  var fullData = %*{
    "variants": data,
    "references": referencesJson
  }

  let variantsFile = db.dbPath / VARIANTS_DB
  let tempFile = variantsFile & ".tmp"

  try:
    # Ensure parent directory exists
    let parentDir = parentDir(variantsFile)
    if not dirExists(parentDir):
      createDir(parentDir)

    # Write to temporary file
    writeFile(tempFile, fullData.pretty())

    # Atomic rename (replaces existing file)
    moveFile(tempFile, variantsFile)

  except IOError as e:
    # Clean up temp file on failure
    if fileExists(tempFile):
      try:
        removeFile(tempFile)
      except:
        discard
    # Re-raise the error
    raise newException(IOError, "Failed to save variants database: " & e.msg)

proc loadVariants*(db: VariantDatabase) =
  ## Load variants from database
  let variantsFile = db.dbPath / VARIANTS_DB
  if not fileExists(variantsFile):
    return  # Empty database

  try:
    let data = parseFile(variantsFile)

    # Check if this is the new format with variants and references
    if data.hasKey("variants") and data.hasKey("references"):
      # New format (Task 14.2)
      let variantsData = data["variants"]
      let referencesData = data["references"]

      # Load variants
      for fingerprint, info in variantsData.pairs:
        # Parse domains
        var domains = initTable[string, seq[string]]()
        if info.hasKey("domains"):
          for domain, values in info["domains"].pairs:
            domains[domain] = values.getElems().mapIt(it.getStr())

        # Parse toolchain
        var toolchain = ToolchainInfo()
        if info.hasKey("toolchain"):
          toolchain.name = info["toolchain"]["name"].getStr()
          toolchain.version = info["toolchain"]["version"].getStr()

        # Parse target
        var target = TargetInfo()
        if info.hasKey("target"):
          target.arch = info["target"]["arch"].getStr()
          target.os = info["target"]["os"].getStr()

        # Create variant record
        var variant = newVariantRecord(
          fingerprint,
          info["packageName"].getStr(),
          info["version"].getStr(),
          info["installPath"].getStr()
        )
        variant.domains = domains
        variant.toolchain = toolchain
        variant.target = target
        variant.installedAt = fromUnix(info["installedAt"].getInt())

        db.variants[fingerprint] = variant

      # Load references
      for fingerprint, refs in referencesData.pairs:
        db.references[fingerprint] = refs.getElems().mapIt(it.getStr())

    else:
      # Old format - just variants (backward compatibility)
      for fingerprint, info in data.pairs:
        # Parse domains
        var domains = initTable[string, seq[string]]()
        if info.hasKey("domains"):
          for domain, values in info["domains"].pairs:
            domains[domain] = values.getElems().mapIt(it.getStr())

        # Parse toolchain
        var toolchain = ToolchainInfo()
        if info.hasKey("toolchain"):
          toolchain.name = info["toolchain"]["name"].getStr()
          toolchain.version = info["toolchain"]["version"].getStr()

        # Parse target
        var target = TargetInfo()
        if info.hasKey("target"):
          target.arch = info["target"]["arch"].getStr()
          target.os = info["target"]["os"].getStr()

        # Create variant record
        var variant = newVariantRecord(
          fingerprint,
          info["packageName"].getStr(),
          info["version"].getStr(),
          info["installPath"].getStr()
        )
        variant.domains = domains
        variant.toolchain = toolchain
        variant.target = target
        variant.installedAt = fromUnix(info["installedAt"].getInt())

        db.variants[fingerprint] = variant

  except JsonParsingError as e:
    echo "Warning: Failed to parse variants database: ", e.msg
  except KeyError as e:
    echo "Warning: Missing field in variants database: ", e.msg

# #############################################################################
# Variant Record Operations
# #############################################################################

proc createVariantRecord*(
  db: VariantDatabase,
  fingerprint: string,
  packageName: string,
  version: string,
  domains: Table[string, seq[string]],
  installPath: string,
  toolchain: ToolchainInfo,
  target: TargetInfo
): bool =
  ## Insert a new variant record into the database
  ## Returns true if successful, false if variant already exists

  if fingerprint in db.variants:
    return false  # Variant already exists

  var variant = newVariantRecord(fingerprint, packageName, version, installPath)
  variant.domains = domains
  variant.toolchain = toolchain
  variant.target = target
  variant.installedAt = getTime()

  db.variants[fingerprint] = variant
  db.saveVariants()

  return true

proc queryVariantByFingerprint*(
  db: VariantDatabase,
  fingerprint: string
): Option[VariantRecord] =
  ## Look up a variant by its fingerprint
  ## Returns Some(record) if found, None if not found
  if fingerprint in db.variants:
    return some(db.variants[fingerprint])
  else:
    return none(VariantRecord)

proc queryVariantByFingerprintLegacy*(
  db: VariantDatabase,
  fingerprint: string
): VariantQueryResult {.deprecated: "Use queryVariantByFingerprint which returns Option[VariantRecord]".} =
  ## DEPRECATED: Use queryVariantByFingerprint instead
  ## Look up a variant by its fingerprint (legacy API)
  if fingerprint in db.variants:
    return VariantQueryResult(
      found: true,
      record: db.variants[fingerprint]
    )
  else:
    return VariantQueryResult(found: false)

proc queryVariantByPath*(
  db: VariantDatabase,
  installPath: string
): Option[VariantRecord] =
  ## Query variant by installation path (Task 14.1)
  ## Used for conflict detection
  ## Returns Some(record) if found, None if not found

  for variant in db.variants.values:
    if variant.installPath == installPath:
      return some(variant)

  return none(VariantRecord)

proc queryVariantByPathLegacy*(
  db: VariantDatabase,
  installPath: string
): VariantQueryResult {.deprecated: "Use queryVariantByPath which returns Option[VariantRecord]".} =
  ## DEPRECATED: Use queryVariantByPath instead
  ## Query variant by installation path (legacy API)

  for variant in db.variants.values:
    if variant.installPath == installPath:
      return VariantQueryResult(
        found: true,
        record: variant
      )

  return VariantQueryResult(found: false)

proc queryVariantsByPackage*(
  db: VariantDatabase,
  packageName: string
): seq[VariantRecord] =
  ## List all variants of a specific package
  result = @[]

  for variant in db.variants.values:
    if variant.packageName == packageName:
      result.add(variant)

proc queryVariantsByPackageVersion*(
  db: VariantDatabase,
  packageName: string,
  version: string
): seq[VariantRecord] =
  ## List all variants of a specific package version
  result = @[]

  for variant in db.variants.values:
    if variant.packageName == packageName and variant.version == version:
      result.add(variant)

proc deleteVariantRecord*(
  db: VariantDatabase,
  fingerprint: string
): bool {.deprecated: "Use deleteVariantWithReferences to safely handle references".} =
  ## DEPRECATED: Use deleteVariantWithReferences instead
  ## Remove a variant record from the database
  ## WARNING: This does not check for references and may cause dangling references
  ## Returns true if successful, false if variant not found

  # Check for references before deleting
  let refs = db.getVariantReferences(fingerprint)
  if refs.len > 0:
    echo "Warning: Deleting variant with active references: ", refs.join(", ")
    echo "Consider using deleteVariantWithReferences instead"

  if fingerprint notin db.variants:
    return false

  db.variants.del(fingerprint)

  # Clean up references
  if fingerprint in db.references:
    db.references.del(fingerprint)

  db.saveVariants()

  return true

proc updateVariantPath*(
  db: VariantDatabase,
  fingerprint: string,
  newPath: string
): bool =
  ## Update the installation path of a variant
  ## Returns true if successful, false if variant not found

  if fingerprint notin db.variants:
    return false

  db.variants[fingerprint].installPath = newPath
  db.saveVariants()

  return true

# #############################################################################
# Query and Statistics Operations
# #############################################################################

proc listAllVariants*(db: VariantDatabase): seq[VariantRecord] =
  ## List all variants in the database
  result = @[]
  for variant in db.variants.values:
    result.add(variant)

proc countVariants*(db: VariantDatabase): int =
  ## Get total number of variants
  db.variants.len

proc countVariantsByPackage*(db: VariantDatabase, packageName: string): int =
  ## Count variants for a specific package
  result = 0
  for variant in db.variants.values:
    if variant.packageName == packageName:
      result += 1

proc hasVariant*(db: VariantDatabase, fingerprint: string): bool =
  ## Check if a variant exists in the database
  fingerprint in db.variants

proc getVariantDomains*(
  db: VariantDatabase,
  fingerprint: string
): Option[Table[string, seq[string]]] =
  ## Get the domain configuration for a variant
  if fingerprint in db.variants:
    return some(db.variants[fingerprint].domains)
  else:
    return none(Table[string, seq[string]])

proc findVariantByPath*(
  db: VariantDatabase,
  installPath: string
): Option[VariantRecord] =
  ## Find a variant by its installation path
  for variant in db.variants.values:
    if variant.installPath == installPath:
      return some(variant)
  return none(VariantRecord)

# #############################################################################
# Utility Functions
# #############################################################################

proc `$`*(qr: VariantQueryResult): string {.deprecated.} =
  ## DEPRECATED: String representation of query result (legacy API)
  if qr.found:
    result = "Found: " & qr.record.fingerprint
  else:
    result = "Not found"

proc prettyPrint*(variant: VariantRecord): string =
  ## Pretty print a variant record
  result = fmt"Variant: {variant.packageName}/{variant.version}" & "\n"
  result.add(fmt"  Fingerprint: {variant.fingerprint}" & "\n")
  result.add(fmt"  Install Path: {variant.installPath}" & "\n")
  let installedTime = variant.installedAt.format("yyyy-MM-dd HH:mm:ss")
  result.add("  Installed: " & installedTime & "\n")
  result.add(fmt"  Toolchain: {variant.toolchain.name}-{variant.toolchain.version}" & "\n")
  result.add(fmt"  Target: {variant.target.arch}-{variant.target.os}" & "\n")

  if variant.domains.len > 0:
    result.add("  Domains:\n")
    for domain, values in variant.domains:
      result.add("    " & domain & ": " & values.join(", ") & "\n")


# #############################################################################
# Variant Reference Tracking (Task 14.2)
# #############################################################################

proc addVariantReference*(
  db: VariantDatabase,
  variantFingerprint: string,
  dependentPackage: string
) =
  ## Add a reference from a package to a variant
  ## Tracks which packages depend on which variants

  if variantFingerprint notin db.references:
    db.references[variantFingerprint] = @[]

  if dependentPackage notin db.references[variantFingerprint]:
    db.references[variantFingerprint].add(dependentPackage)
    db.saveVariants()

proc removeVariantReference*(
  db: VariantDatabase,
  variantFingerprint: string,
  dependentPackage: string
): bool =
  ## Remove a reference from a package to a variant
  ## Returns true if reference was removed

  if variantFingerprint notin db.references:
    return false

  let idx = db.references[variantFingerprint].find(dependentPackage)
  if idx >= 0:
    db.references[variantFingerprint].delete(idx)

    # Clean up empty reference lists
    if db.references[variantFingerprint].len == 0:
      db.references.del(variantFingerprint)

    db.saveVariants()
    return true

  return false

proc getVariantReferences*(
  db: VariantDatabase,
  variantFingerprint: string
): seq[string] =
  ## Get list of packages that reference this variant

  if variantFingerprint in db.references:
    return db.references[variantFingerprint]
  else:
    return @[]

proc getVariantReferenceInfo*(
  db: VariantDatabase,
  variantFingerprint: string
): VariantReferenceInfo =
  ## Get detailed reference information for a variant

  result = VariantReferenceInfo(
    fingerprint: variantFingerprint,
    referencedBy: db.getVariantReferences(variantFingerprint),
    canDelete: false
  )

  # Can only delete if no references
  result.canDelete = result.referencedBy.len == 0

proc canDeleteVariant*(
  db: VariantDatabase,
  variantFingerprint: string
): bool =
  ## Check if a variant can be safely deleted
  ## Returns false if other packages depend on it

  let refs = db.getVariantReferences(variantFingerprint)
  return refs.len == 0

proc deleteVariantWithReferences*(
  db: VariantDatabase,
  variantFingerprint: string,
  force: bool = false
): tuple[success: bool, message: string] =
  ## Delete a variant, checking for references
  ## If force=true, deletes even if referenced (dangerous!)

  # Check if variant exists
  if variantFingerprint notin db.variants:
    return (false, "Variant not found: " & variantFingerprint)

  # Check references
  let refs = db.getVariantReferences(variantFingerprint)
  if refs.len > 0 and not force:
    let refList = refs.join(", ")
    return (false, "Cannot delete: variant is referenced by: " & refList)

  # Delete variant
  db.variants.del(variantFingerprint)

  # Clean up references
  if variantFingerprint in db.references:
    db.references.del(variantFingerprint)

  db.saveVariants()

  if force and refs.len > 0:
    return (true, "Variant deleted (forced, was referenced by " & $refs.len & " packages)")
  else:
    return (true, "Variant deleted successfully")

proc listReferencedVariants*(
  db: VariantDatabase,
  packageName: string
): seq[string] =
  ## List all variants referenced by a specific package

  result = @[]
  for fingerprint, refs in db.references.pairs:
    if packageName in refs:
      result.add(fingerprint)
