## Delta Dedup & Smart Fetch Module for NexusForge
##
## This module provides:
## - Local deduplication before fetching (check CAS first)
## - Smart delta fetch (only fetch missing chunks using HTTP Range)
## - Manifest-based artifact resolution
##
## Leverages existing infrastructure:
## - `cas.nim` for object storage and dedup
## - `remote/resumable_fetch.nim` for HTTP Range downloads
## - `remote/manager.nim` for repository management

import std/[options, sequtils, tables, json, strformat, os, httpclient, times, asyncdispatch]
import ../cas
import ../remote/resumable_fetch
import ../security/hash_verifier

type
  DeltaFetchResult* = object
    success*: bool
    totalBytes*: int64          # Total package size
    downloadedBytes*: int64     # Bytes actually downloaded
    dedupedBytes*: int64        # Bytes from local CAS (dedup)
    chunks*: seq[ChunkStatus]   # Status of each chunk
    casHash*: string            # Final CAS hash
    errors*: seq[string]

  ChunkStatus* = object
    hash*: string
    size*: int
    source*: ChunkSource        # Where chunk came from

  ChunkSource* = enum
    FromLocalCas,               # Already had it locally
    FromRemote,                 # Downloaded from remote
    FromCache                   # From local cache

  PackageManifest* = object
    ## Remote package manifest describing chunks
    name*: string
    version*: string
    totalSize*: int64
    rootHash*: string           # Hash of the assembled package
    chunks*: seq[ManifestChunk]
    signature*: string          # Ed25519 signature of manifest

  ManifestChunk* = object
    hash*: string
    offset*: int64
    size*: int
    url*: string                # Optional direct URL

  SmartFetcher* = ref object
    cas*: CasManager
    cacheDir*: string
    maxConcurrent*: int
    preferLocalDedup*: bool
    progressCallback*: proc(downloaded, total: int64)

# =============================================================================
# Local Deduplication
# =============================================================================

proc checkLocalChunks*(fetcher: SmartFetcher, manifest: PackageManifest): 
    tuple[local: seq[ManifestChunk], missing: seq[ManifestChunk]] =
  ## Check which chunks already exist locally in CAS
  ## Returns (locally available, need to fetch)
  
  var local: seq[ManifestChunk] = @[]
  var missing: seq[ManifestChunk] = @[]
  
  for chunk in manifest.chunks:
    if fetcher.cas.objectExists(chunk.hash):
      local.add(chunk)
    else:
      missing.add(chunk)
  
  return (local, missing)

proc calculateDedupSavings*(fetcher: SmartFetcher, manifest: PackageManifest): 
    tuple[totalSize: int64, dedupSize: int64, ratio: float] =
  ## Calculate how much data can be skipped due to local dedup
  let (local, missing) = fetcher.checkLocalChunks(manifest)
  
  let totalSize = manifest.totalSize
  let dedupSize = local.mapIt(it.size.int64).foldl(a + b, 0'i64)
  let ratio = if totalSize > 0: dedupSize.float / totalSize.float else: 0.0
  
  return (totalSize, dedupSize, ratio)

# =============================================================================
# Smart Delta Fetch
# =============================================================================

proc fetchMissingChunks*(fetcher: SmartFetcher, baseUrl: string, 
                        chunks: seq[ManifestChunk]): Future[DeltaFetchResult] {.async.} =
  ## Fetch only the chunks that are missing locally
  var result = DeltaFetchResult(success: true)
  
  for chunk in chunks:
    try:
      let chunkUrl = if chunk.url.len > 0: chunk.url 
                     else: baseUrl & "/chunks/" & chunk.hash
      
      # Create download session for this chunk
      let tempPath = fetcher.cacheDir / chunk.hash
      var session = newDownloadSession(chunkUrl, tempPath, fetcher.cas)
      
      # Download the chunk
      let fetchResult = await session.downloadWithResume(fetcher.maxConcurrent)
      
      if fetchResult.success:
        # Store in CAS
        let data = readFile(tempPath)
        let dataBytes = data.toOpenArrayByte(0, data.len - 1).toSeq()
        let storeResult = fetcher.cas.storeObject(dataBytes)
        
        if storeResult.isOk:
          result.chunks.add(ChunkStatus(
            hash: chunk.hash,
            size: chunk.size,
            source: FromRemote
          ))
          result.downloadedBytes += chunk.size.int64
        else:
          result.errors.add("Failed to store chunk " & chunk.hash)
          result.success = false
        
        # Clean up temp file
        if fileExists(tempPath):
          removeFile(tempPath)
      else:
        result.errors.add("Failed to download chunk " & chunk.hash & ": " & fetchResult.error)
        result.success = false
      
    except Exception as e:
      result.errors.add("Exception fetching chunk " & chunk.hash & ": " & e.msg)
      result.success = false
  
  return result

proc smartFetchPackage*(fetcher: SmartFetcher, baseUrl: string, 
                       manifest: PackageManifest): Future[DeltaFetchResult] {.async.} =
  ## Smart fetch: check local CAS first, then fetch only missing chunks
  var result = DeltaFetchResult(success: true, totalBytes: manifest.totalSize)
  
  # Step 1: Check local dedup
  let (local, missing) = fetcher.checkLocalChunks(manifest)
  
  # Record locally available chunks
  for chunk in local:
    result.chunks.add(ChunkStatus(
      hash: chunk.hash,
      size: chunk.size,
      source: FromLocalCas
    ))
    result.dedupedBytes += chunk.size.int64
  
  # Step 2: Fetch missing chunks
  if missing.len > 0:
    let fetchResult = await fetcher.fetchMissingChunks(baseUrl, missing)
    result.downloadedBytes = fetchResult.downloadedBytes
    result.chunks.add(fetchResult.chunks)
    
    if not fetchResult.success:
      result.success = false
      result.errors = fetchResult.errors
  
  # Step 3: Set final hash
  if result.success:
    result.casHash = manifest.rootHash
  
  return result

# =============================================================================
# Manifest Handling
# =============================================================================

proc parsePackageManifest*(json: JsonNode): PackageManifest =
  ## Parse package manifest from JSON
  result = PackageManifest(
    name: json["name"].getStr(),
    version: json["version"].getStr(),
    totalSize: json["total_size"].getBiggestInt(),
    rootHash: json["root_hash"].getStr(),
    signature: json.getOrDefault("signature").getStr("")
  )
  
  for chunk in json["chunks"]:
    result.chunks.add(ManifestChunk(
      hash: chunk["hash"].getStr(),
      offset: chunk["offset"].getBiggestInt(),
      size: chunk["size"].getInt(),
      url: chunk.getOrDefault("url").getStr("")
    ))

proc fetchManifest*(url: string): Future[Option[PackageManifest]] {.async.} =
  ## Fetch and parse package manifest from URL
  try:
    let client = newAsyncHttpClient()
    defer: client.close()
    
    let response = await client.getContent(url)
    let json = parseJson(response)
    return some(parsePackageManifest(json))
  except:
    return none(PackageManifest)

# =============================================================================
# Fetcher Factory
# =============================================================================

proc newSmartFetcher*(cas: CasManager, cacheDir: string = ""): SmartFetcher =
  ## Create a new smart fetcher
  let actualCache = if cacheDir.len > 0: cacheDir else: getTempDir() / "nip-delta-cache"
  createDir(actualCache)
  
  SmartFetcher(
    cas: cas,
    cacheDir: actualCache,
    maxConcurrent: 4,
    preferLocalDedup: true
  )

# =============================================================================
# High-Level API
# =============================================================================

proc fetchWithDedup*(fetcher: SmartFetcher, packageUrl: string): Future[DeltaFetchResult] {.async.} =
  ## High-level API: fetch package manifest and use smart delta fetch
  
  # Fetch manifest
  let manifestUrl = packageUrl & ".manifest.json"
  let manifestOpt = await fetchManifest(manifestUrl)
  
  if manifestOpt.isNone:
    return DeltaFetchResult(
      success: false,
      errors: @["Failed to fetch manifest from " & manifestUrl]
    )
  
  let manifest = manifestOpt.get()
  
  # Calculate savings
  let (totalSize, dedupSize, ratio) = fetcher.calculateDedupSavings(manifest)
  echo fmt"📦 Package: {manifest.name} v{manifest.version}"
  echo fmt"📊 Total: {totalSize} bytes, Local: {dedupSize} bytes ({ratio*100:.1f}% dedup)"
  
  # Smart fetch
  let baseUrl = packageUrl.parentDir
  return await fetcher.smartFetchPackage(baseUrl, manifest)

proc assembleFromCas*(fetcher: SmartFetcher, manifest: PackageManifest, 
                     outputPath: string): bool =
  ## Assemble package from CAS chunks
  try:
    var outputFile = open(outputPath, fmWrite)
    defer: outputFile.close()
    
    for chunk in manifest.chunks:
      let dataResult = fetcher.cas.retrieveObject(chunk.hash)
      if dataResult.isOk:
        let data = dataResult.value
        discard outputFile.writeBytes(data, 0, data.len)
      else:
        return false
    
    return true
  except:
    return false
