# 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.

## variant_fingerprint.nim
## Variant fingerprint calculation using BLAKE2b
## Provides deterministic content-addressed identifiers for package variants

import std/[tables, algorithm, strutils, sequtils]
import nimcrypto/hash
import nimcrypto/blake2
import variant_types
import config  # For CompilerFlags

# #############################################################################
# BLAKE2b String Hashing
# #############################################################################

proc calculateBlake2bString*(input: string): string =
  ## Calculate BLAKE2b hash of a string and return it in the format "blake2b-[hash]"
  ## This is similar to calculateBlake2b but works on strings instead of files
  ## Uses BLAKE2b-256 (32 bytes) for shorter fingerprints
  try:
    let digest = blake2_256.digest(input)
    var hexDigest = ""
    for b in digest.data:
      hexDigest.add(b.toHex(2).toLowerAscii())
    result = "blake2b-" & hexDigest
  except Exception as e:
    raise newException(ValueError, "Failed to calculate BLAKE2b hash: " & e.msg)

# #############################################################################
# Variant Fingerprint Calculation
# #############################################################################

proc calculateVariantFingerprint*(
  packageName: string,
  version: string,
  domains: Table[string, seq[string]],
  compilerFlags: CompilerFlags,
  toolchain: ToolchainInfo,
  target: TargetInfo
): string =
  ## Calculate deterministic BLAKE2b hash for variant
  ##
  ## This function ensures:
  ## - Identical inputs always produce identical fingerprints
  ## - Different inputs produce different fingerprints
  ## - Reproducible across systems and time
  ##
  ## Returns: blake2b-[12-char-prefix]

  var hashInput = ""

  # 1. Package identity
  hashInput.add(packageName & "\n")
  hashInput.add(version & "\n")

  # 2. Sorted domain flags (deterministic ordering)
  var sortedDomains = toSeq(domains.keys).sorted()
  for domain in sortedDomains:
    hashInput.add(domain & ":")
    var sortedFlags = domains[domain].sorted()
    hashInput.add(sortedFlags.join(",") & "\n")

  # 3. Compiler flags
  hashInput.add("cflags:" & compilerFlags.cflags & "\n")
  hashInput.add("ldflags:" & compilerFlags.ldflags & "\n")

  # 4. Toolchain
  hashInput.add("toolchain:" & toolchain.name & "-" & toolchain.version & "\n")

  # 5. Target
  hashInput.add("target:" & target.arch & "-" & target.os & "\n")

  # Calculate BLAKE2b hash
  let fullHash = calculateBlake2bString(hashInput)

  # Return format: blake2b-[12-char-prefix]
  # fullHash format is "blake2b-[64-char-hex]"
  # We want "blake2b-" (8 chars) + 12 chars = 20 chars total
  if fullHash.len >= 20:
    result = fullHash[0..19]
  else:
    result = fullHash


proc buildVariantFingerprint*(
  packageName: string,
  version: string,
  domains: Table[string, seq[string]],
  compilerFlags: CompilerFlags,
  toolchain: ToolchainInfo,
  target: TargetInfo
): VariantFingerprint =
  ## Build a complete VariantFingerprint object with calculated hash

  let hash = calculateVariantFingerprint(
    packageName, version, domains, compilerFlags, toolchain, target
  )

  result = VariantFingerprint(
    packageName: packageName,
    version: version,
    domainFlags: domains,
    compilerFlags: compilerFlags,
    toolchain: toolchain,
    target: target,
    hash: hash
  )

# #############################################################################
# Fingerprint Validation and Utilities
# #############################################################################

proc isValidFingerprint*(fingerprint: string): bool =
  ## Validate fingerprint format: blake2b-[12-hex-chars]
  if fingerprint.len != 20:
    return false

  if not fingerprint.startsWith("blake2b-"):
    return false

  # Check that remaining chars are hex
  let hexPart = fingerprint[8..^1]
  for c in hexPart:
    if c notin {'0'..'9', 'a'..'f', 'A'..'F'}:
      return false

  return true

proc extractFingerprintPrefix*(fullFingerprint: string): string =
  ## Extract 12-char prefix from full fingerprint
  ## Input: "blake2b-[64-char-hex]"
  ## Output: "blake2b-[12-char]"

  if fullFingerprint.len >= 20 and fullFingerprint.startsWith("blake2b-"):
    result = fullFingerprint[0..19]
  else:
    result = fullFingerprint

proc compareFingerprintInputs*(
  fp1: VariantFingerprint,
  fp2: VariantFingerprint
): seq[string] =
  ## Compare two fingerprints and return list of differences
  result = @[]

  if fp1.packageName != fp2.packageName:
    result.add("packageName: " & fp1.packageName & " vs " & fp2.packageName)

  if fp1.version != fp2.version:
    result.add("version: " & fp1.version & " vs " & fp2.version)

  # Compare domains
  var allDomains: seq[string] = @[]
  for domain in fp1.domainFlags.keys:
    if domain notin allDomains:
      allDomains.add(domain)
  for domain in fp2.domainFlags.keys:
    if domain notin allDomains:
      allDomains.add(domain)

  for domain in allDomains:
    let flags1 = if fp1.domainFlags.hasKey(domain): fp1.domainFlags[domain] else: @[]
    let flags2 = if fp2.domainFlags.hasKey(domain): fp2.domainFlags[domain] else: @[]

    if flags1 != flags2:
      result.add("domain." & domain & ": " & flags1.join(",") & " vs " & flags2.join(","))

  # Compare compiler flags
  if fp1.compilerFlags.cflags != fp2.compilerFlags.cflags:
    result.add("cflags: " & fp1.compilerFlags.cflags & " vs " & fp2.compilerFlags.cflags)

  if fp1.compilerFlags.ldflags != fp2.compilerFlags.ldflags:
    result.add("ldflags: " & fp1.compilerFlags.ldflags & " vs " & fp2.compilerFlags.ldflags)

  # Compare toolchain
  if fp1.toolchain != fp2.toolchain:
    result.add("toolchain: " & $fp1.toolchain & " vs " & $fp2.toolchain)

  # Compare target
  if fp1.target != fp2.target:
    result.add("target: " & $fp1.target & " vs " & $fp2.target)

# #############################################################################
# Debug and Inspection
# #############################################################################

proc getFingerprintInputString*(
  packageName: string,
  version: string,
  domains: Table[string, seq[string]],
  compilerFlags: CompilerFlags,
  toolchain: ToolchainInfo,
  target: TargetInfo
): string =
  ## Get the exact string that will be hashed for fingerprint calculation
  ## Useful for debugging and understanding what contributes to the hash

  result = ""

  # 1. Package identity
  result.add(packageName & "\n")
  result.add(version & "\n")

  # 2. Sorted domain flags
  var sortedDomains = toSeq(domains.keys).sorted()
  for domain in sortedDomains:
    result.add(domain & ":")
    var sortedFlags = domains[domain].sorted()
    result.add(sortedFlags.join(",") & "\n")

  # 3. Compiler flags
  result.add("cflags:" & compilerFlags.cflags & "\n")
  result.add("ldflags:" & compilerFlags.ldflags & "\n")

  # 4. Toolchain
  result.add("toolchain:" & toolchain.name & "-" & toolchain.version & "\n")

  # 5. Target
  result.add("target:" & target.arch & "-" & target.os & "\n")

proc debugFingerprint*(fp: VariantFingerprint): string =
  ## Generate debug output for a fingerprint
  result = "VariantFingerprint:\n"
  result.add("  Package: " & fp.packageName & " " & fp.version & "\n")
  result.add("  Hash: " & fp.hash & "\n")
  result.add("  Domains:\n")

  var sortedDomains = toSeq(fp.domainFlags.keys).sorted()
  for domain in sortedDomains:
    result.add("    " & domain & ": " & fp.domainFlags[domain].join(", ") & "\n")

  result.add("  Compiler Flags:\n")
  result.add("    cflags: " & fp.compilerFlags.cflags & "\n")
  result.add("    ldflags: " & fp.compilerFlags.ldflags & "\n")
  result.add("  Toolchain: " & $fp.toolchain & "\n")
  result.add("  Target: " & $fp.target & "\n")
