## nimpak/security/hash_verifier.nim
## Streaming hash verification module for NimPak
##
## This module implements memory-efficient hash verification for packages of any size.
## Supports BLAKE2b (primary) and BLAKE3 (future) with algorithm detection and fallback.

import std/[os, streams, strutils, strformat, times, options]
import nimcrypto/blake2

type
  HashAlgorithm* = enum
    HashBlake2b = "blake2b"
    HashBlake3 = "blake3" # Future implementation

  HashResult* = object
    algorithm*: HashAlgorithm
    digest*: string
    verified*: bool
    computeTime*: float # Seconds taken to compute

  HashVerificationError* = object of CatchableError
    algorithm*: HashAlgorithm
    expectedHash*: string
    actualHash*: string

  StreamingHasher* = object
    algorithm*: HashAlgorithm
    blake2bContext*: blake2_512 # BLAKE2b-512 context
                                # blake3Context*: Blake3Context  # Future BLAKE3 context
    bytesProcessed*: int64
    startTime*: times.DateTime

# =============================================================================
# Hash Algorithm Detection and Parsing
# =============================================================================

proc detectHashAlgorithm*(hashString: string): HashAlgorithm =
  ## Detect hash algorithm from hash string format
  if hashString.startsWith("blake2b-"):
    return HashBlake2b
  elif hashString.startsWith("blake3-"):
    return HashBlake3
  elif hashString.len == 128: # BLAKE2b-512 hex length
    return HashBlake2b
  else:
    raise newException(ValueError, fmt"Unknown hash format: {hashString[0..min(50, hashString.high)]}")

proc parseHashString*(hashString: string): (HashAlgorithm, string) =
  ## Parse hash string into algorithm and digest
  let algorithm = detectHashAlgorithm(hashString)

  case algorithm:
  of HashBlake2b:
    if hashString.startsWith("blake2b-"):
      return (HashBlake2b, hashString[8..^1])
    else:
      return (HashBlake2b, hashString)

  of HashBlake3:
    if hashString.startsWith("blake3-"):
      return (HashBlake3, hashString[7..^1])
    else:
      return (HashBlake3, hashString)

proc formatHashString*(algorithm: HashAlgorithm, digest: string): string =
  ## Format hash digest with algorithm prefix
  case algorithm:
  of HashBlake2b: fmt"blake2b-{digest}"
  of HashBlake3: fmt"blake3-{digest}"

# =============================================================================
# Streaming Hash Computation
# =============================================================================

proc initStreamingHasher*(algorithm: HashAlgorithm): StreamingHasher =
  ## Initialize a streaming hasher for the specified algorithm
  var hasher = StreamingHasher(
    algorithm: algorithm,
    bytesProcessed: 0,
    startTime: now()
  )

  case algorithm:
  of HashBlake2b:
    hasher.blake2bContext.init()

  of HashBlake3:
    # TODO: Implement BLAKE3 when library is available
    # For now, fallback to BLAKE2b with a warning
    echo "Warning: BLAKE3 not available, falling back to BLAKE2b"
    hasher.algorithm = HashBlake2b
    hasher.blake2bContext.init()

  return hasher

proc update*(hasher: var StreamingHasher, data: openArray[byte]) =
  ## Update hasher with new data chunk
  case hasher.algorithm:
  of HashBlake2b:
    hasher.blake2bContext.update(data)

  of HashBlake3:
    # Fallback to BLAKE2b (already handled in init)
    hasher.blake2bContext.update(data)

  hasher.bytesProcessed += data.len

proc update*(hasher: var StreamingHasher, data: string) =
  ## Update hasher with string data
  hasher.update(data.toOpenArrayByte(0, data.high))

proc finalize*(hasher: var StreamingHasher): HashResult =
  ## Finalize hash computation and return result
  let endTime = now()
  let computeTime = (endTime - hasher.startTime).inMilliseconds.float / 1000.0

  case hasher.algorithm:
  of HashBlake2b:
    let digest = hasher.blake2bContext.finish()
    return HashResult(
      algorithm: HashBlake2b,
      digest: ($digest).toLower(), # Ensure lowercase hex
      verified: false, # Will be set by verification function
      computeTime: computeTime
    )

  of HashBlake3:
    # Fallback to BLAKE2b (already handled in init)
    let digest = hasher.blake2bContext.finish()
    return HashResult(
      algorithm: HashBlake2b, # Report actual algorithm used
      digest: ($digest).toLower(), # Ensure lowercase hex
      verified: false,
      computeTime: computeTime
    )

# =============================================================================
# File Hash Computation
# =============================================================================

const
  CHUNK_SIZE = 64 * 1024                    # 64KB chunks for memory efficiency
  LARGE_FILE_CHUNK_SIZE = 1024 * 1024       # 1MB chunks for large files (>1GB)
  LARGE_FILE_THRESHOLD = 1024 * 1024 * 1024 # 1GB threshold

proc computeFileHash*(filePath: string, algorithm: HashAlgorithm = HashBlake2b): HashResult =
  ## Compute hash of a file using streaming approach with optimized chunk size
  if not fileExists(filePath):
    raise newException(IOError, fmt"File not found: {filePath}")

  # Get file size to determine optimal chunk size
  let fileSize = getFileSize(filePath)
  let chunkSize = if fileSize > LARGE_FILE_THRESHOLD: LARGE_FILE_CHUNK_SIZE else: CHUNK_SIZE

  var hasher = initStreamingHasher(algorithm)
  var fileStream = newFileStream(filePath, fmRead)

  if fileStream.isNil:
    raise newException(IOError, fmt"Cannot open file: {filePath}")

  try:
    var buffer = newString(chunkSize)

    while not fileStream.atEnd():
      let bytesRead = fileStream.readData(buffer[0].addr, chunkSize)
      if bytesRead > 0:
        hasher.update(buffer.toOpenArrayByte(0, bytesRead - 1))

    return hasher.finalize()

  finally:
    fileStream.close()

proc computeLargeFileHash*(filePath: string, algorithm: HashAlgorithm = HashBlake2b,
                          progressCallback: proc(bytesProcessed: int64,
                              totalBytes: int64) = nil): HashResult =
  ## Compute hash of a large file (>1GB) with progress reporting
  if not fileExists(filePath):
    raise newException(IOError, fmt"File not found: {filePath}")

  let fileSize = getFileSize(filePath)
  var hasher = initStreamingHasher(algorithm)
  var fileStream = newFileStream(filePath, fmRead)

  if fileStream.isNil:
    raise newException(IOError, fmt"Cannot open file: {filePath}")

  try:
    var buffer = newString(LARGE_FILE_CHUNK_SIZE)
    var totalProcessed: int64 = 0

    while not fileStream.atEnd():
      let bytesRead = fileStream.readData(buffer[0].addr, LARGE_FILE_CHUNK_SIZE)
      if bytesRead > 0:
        hasher.update(buffer.toOpenArrayByte(0, bytesRead - 1))
        totalProcessed += bytesRead

        # Call progress callback if provided
        if progressCallback != nil:
          progressCallback(totalProcessed, fileSize)

    return hasher.finalize()

  finally:
    fileStream.close()

proc computeStringHash*(data: string, algorithm: HashAlgorithm = HashBlake2b): HashResult =
  ## Compute hash of string data
  var hasher = initStreamingHasher(algorithm)
  hasher.update(data)
  return hasher.finalize()

proc computeStreamHash*(stream: Stream, algorithm: HashAlgorithm = HashBlake2b): HashResult =
  ## Compute hash of stream data
  var hasher = initStreamingHasher(algorithm)
  var buffer = newString(CHUNK_SIZE)

  while not stream.atEnd():
    let bytesRead = stream.readData(buffer[0].addr, CHUNK_SIZE)
    if bytesRead > 0:
      hasher.update(buffer.toOpenArrayByte(0, bytesRead - 1))

  return hasher.finalize()

# =============================================================================
# Hash Verification
# =============================================================================

proc verifyFileHash*(filePath: string, expectedHash: string): HashResult =
  ## Verify file hash against expected value
  let (algorithm, expectedDigest) = parseHashString(expectedHash)
  var hashResult = computeFileHash(filePath, algorithm)

  hashResult.verified = (hashResult.digest == expectedDigest)

  if not hashResult.verified:
    var error = newException(HashVerificationError,
        fmt"Hash verification failed for {filePath}")
    error.algorithm = algorithm
    error.expectedHash = expectedDigest
    error.actualHash = hashResult.digest
    raise error

  return hashResult

proc verifyStringHash*(data: string, expectedHash: string): HashResult =
  ## Verify string hash against expected value
  let (algorithm, expectedDigest) = parseHashString(expectedHash)
  var hashResult = computeStringHash(data, algorithm)

  hashResult.verified = (hashResult.digest == expectedDigest)

  if not hashResult.verified:
    var error = newException(HashVerificationError,
        fmt"Hash verification failed for string data")
    error.algorithm = algorithm
    error.expectedHash = expectedDigest
    error.actualHash = hashResult.digest
    raise error

  return hashResult

proc verifyStreamHash*(stream: Stream, expectedHash: string): HashResult =
  ## Verify stream hash against expected value
  let (algorithm, expectedDigest) = parseHashString(expectedHash)
  var hashResult = computeStreamHash(stream, algorithm)

  hashResult.verified = (hashResult.digest == expectedDigest)

  if not hashResult.verified:
    var error = newException(HashVerificationError,
        fmt"Hash verification failed for stream data")
    error.algorithm = algorithm
    error.expectedHash = expectedDigest
    error.actualHash = hashResult.digest
    raise error

  return hashResult

# =============================================================================
# Batch Verification
# =============================================================================

type
  FileHashEntry* = object
    filePath*: string
    expectedHash*: string
    result*: Option[HashResult]
    error*: string

proc verifyMultipleFiles*(entries: var seq[FileHashEntry]): tuple[verified: int, failed: int] =
  ## Verify multiple files in batch
  var verified = 0
  var failed = 0

  for entry in entries.mitems:
    try:
      entry.result = some(verifyFileHash(entry.filePath, entry.expectedHash))
      if entry.result.get().verified:
        inc verified
      else:
        inc failed
        entry.error = "Hash mismatch"
    except Exception as e:
      inc failed
      entry.error = e.msg
      entry.result = none(HashResult)

  return (verified, failed)

# =============================================================================
# Performance and Statistics
# =============================================================================

proc formatHashRate*(bytesProcessed: int64, computeTime: float): string =
  ## Format hash computation rate in human-readable format
  if computeTime <= 0.0:
    result = "N/A"
  else:
    let bytesPerSecond = bytesProcessed.float / computeTime

    if bytesPerSecond >= 1_000_000_000:
      result = fmt"{bytesPerSecond / 1_000_000_000:.1f} GB/s"
    elif bytesPerSecond >= 1_000_000:
      result = fmt"{bytesPerSecond / 1_000_000:.1f} MB/s"
    elif bytesPerSecond >= 1_000:
      result = fmt"{bytesPerSecond / 1_000:.1f} KB/s"
    else:
      result = fmt"{bytesPerSecond.int} B/s"

proc getHashStatistics*(hashResult: HashResult, fileSize: int64): string =
  ## Get formatted hash computation statistics
  let rate = formatHashRate(fileSize, hashResult.computeTime)
  return fmt"Algorithm: {hashResult.algorithm}, Time: {hashResult.computeTime:.3f}s, Rate: {rate}"

# =============================================================================
# Utility Functions
# =============================================================================

proc isValidHashString*(hashString: string): bool =
  ## Check if hash string is valid
  try:
    discard detectHashAlgorithm(hashString)
    return true
  except ValueError:
    return false

proc getPreferredHashAlgorithm*(): HashAlgorithm =
  ## Get the preferred hash algorithm for new packages
  return HashBlake2b # Primary algorithm

proc getSupportedAlgorithms*(): seq[HashAlgorithm] =
  ## Get list of supported hash algorithms
  return @[HashBlake2b] # Add HashBlake3 when implemented

proc getFallbackAlgorithm*(algorithm: HashAlgorithm): HashAlgorithm =
  ## Get fallback algorithm for unsupported algorithms
  case algorithm:
  of HashBlake3:
    return HashBlake2b # BLAKE3 falls back to BLAKE2b
  of HashBlake2b:
    return algorithm # Already supported

proc isAlgorithmSupported*(algorithm: HashAlgorithm): bool =
  ## Check if algorithm is natively supported (no fallback needed)
  return algorithm in getSupportedAlgorithms() and algorithm != HashBlake3

# =============================================================================
# Export main functions
# =============================================================================

export HashAlgorithm, HashResult, HashVerificationError, StreamingHasher
export detectHashAlgorithm, parseHashString, formatHashString
export initStreamingHasher, update, finalize
export computeFileHash, computeLargeFileHash, computeStringHash, computeStreamHash
export verifyFileHash, verifyStringHash, verifyStreamHash
export verifyMultipleFiles, FileHashEntry
export formatHashRate, getHashStatistics
export isValidHashString, getPreferredHashAlgorithm, getSupportedAlgorithms
export getFallbackAlgorithm, isAlgorithmSupported
