## nimpak/cache/selection.nim
## Intelligent binary selection for NimPak cache system
##
## This module provides optimal binary selection with:
## - Multi-criteria decision making
## - Performance optimization preferences
## - Size vs speed trade-offs
## - Fallback strategies for source builds
## - User preference integration

import std/[algorithm, sequtils, strformat, tables, math]
import compatibility

type
  SelectionCriteria* = enum
    CriteriaCompatibility = "compatibility"
    CriteriaPerformance = "performance"
    CriteriaSize = "size"
    CriteriaAge =
    CriteriaTrust = "trust"

  SelectionStrategy* = enum
    StrategyOptimal = "optimal"        # Best overall match
    StrategyPerformance = "performance" # Prioritize performance
    StrategySize = "size"              # Prioritize smaller size
    StrategyConservative = "conservative" # Most compatible
    StrategyLatest = "latest"          # Newest binary

  SelectionPreferences* = object
    strategy*: SelectionStrategy
    criteriaWeights*: Table[SelectionCriteria, float]
    preferNative*: bool            # Prefer native optimizations
    allowFallback*: bool           # Allow fallback to source build
    maxSizeIncrease*: float        # Max size increase for performance (ratio)
    minCompatibilityScore*: float  # Minimum compatibility score

  BinaryScore* = object
    binary*: BinaryCacheEntry
    compatibilityScore*: float
    performanceScore*: float
    sizeScore*: float
    ageScore*: float
    trustScore*: float
    overallScore*: float
    reasons*: seq[string]

  SelectionResult* = object
    case success*: bool
    of true:
      selectedBinary*: BinaryCacheEntry
      score*: BinaryScore
      alternatives*: seq[BinaryScore]
    of false:
      error*: string
      errorCode*: int
      fallbackToSource*: bool

  BinarySelector* = object
    preferences*: SelectionPreferences
    compatibilityDetector*: CompatibilityDetector

# =============================================================================
# Binary Selector Initialization
# =============================================================================

proc newBinarySelector*(preferences: SelectionPreferences = getDefaultSelectionPreferences()): BinarySelector =
  ## Create a new binary selector
  BinarySelector(
    preferences: preferences,
    compatibilityDetector: newCompatibilityDetector()
  )

proc getDefaultSelectionPreferences*(): SelectionPreferences =
  ## Get default selection preferences
  var weights = initTable[SelectionCriteria, float]()
  weights[CriteriaCompatibility] = 1.0
  weights[CriteriaPerformance] = 0.8
  weights[CriteriaSize] = 0.3
  weights[CriteriaAge] = 0.2
  weights[CriteriaTrust] = 0.6

  SelectionPreferences(
    strategy: StrategyOptimal,
    criteriaWeights: weights,
    preferNative: true,
    allowFallback: true,
    maxSizeIncrease: 2.0,          # Allow 2x size increase for performance
    minCompatibilityScore: 0.7
  )

# =============================================================================
# Scoring Functions
# =============================================================================

proc calculateCompatibilityScore*(selector: BinarySelector, binary: BinaryCacheEntry,
                                 system: SystemInfo): float =
  ## Calculate compatibility score for binary
  let result = selector.compatibilityDetector.checkCompatibility(binary.compatibility, system)
  return result.score

proc calculatePerformanceScore*(binary: BinaryCacheEntry, system: SystemInfo): float =
  ## Calculate performance score based on optimizations
  var score = 0.5  # Base score

  # Architecture-specific optimizations
  if binary.compatibility.architecture == system.architecture:
    score += 0.2

  # CPU feature optimizations
  let binaryFeatures = binary.compatibility.cpuFeatures.toHashSet()
  let systemFeatures = system.availableCpuFeatures.toHashSet()
  let usedFeatures = binaryFeatures * systemFeatures

  # Bonus for using advanced features
  let advancedFeatures = ["avx2", "avx512", "fma", "bmi2"].toHashSet()
  let usedAdvanced = usedFeatures * advancedFeatures
  score += usedAdvanced.len.float * 0.1

  # Allocator performance bonus
  case binary.compatibility.allocator:
  of AllocJemalloc: score += 0.15
  of AllocTcmalloc: score += 0.1
  of AllocMimalloc: score += 0.12
  of AllocDefault: score += 0.0

  # Native compilation bonus
  if "native" in binary.metadata.buildFlags:
    score += 0.1

  return min(1.0, score)

proc calculateSizeScore*(binary: BinaryCacheEntry): float =
  ## Calculate size score (smaller is better)
  let sizeInMB = binary.size.float / (1024.0 * 1024.0)

  # Score based on size ranges
  if sizeInMB <= 1.0:
    return 1.0
  elif sizeInMB <= 5.0:
    return 0.9
  elif sizeInMB <= 10.0:
    return 0.8
  elif sizeInMB <= 50.0:
    return 0.6
  elif sizeInMB <= 100.0:
    return 0.4
  else:
    return 0.2

proc calculateAgeScore*(binary: BinaryCacheEntry): float =
  ## Calculate age score (newer is better)
  let ageInDays = (now() - binary.createdAt).inDays.float

  if ageInDays <= 1.0:
    return 1.0
  elif ageInDays <= 7.0:
    return 0.9
  elif ageInDays <= 30.0:
    return 0.7
  elif ageInDays <= 90.0:
    return 0.5
  else:
    return 0.3

proc calculateTrustScore*(binary: BinaryCacheEntry): float =
  ## Calculate trust score based on metadata
  var score = 0.5  # Base score

  # Verified binary bonus
  if binary.verified:
    score += 0.3

  # Official build bonus
  if "official" in binary.metadata.buildTags:
    score += 0.2

  # Reproducible build bonus
  if binary.metadata.reproducible:
    score += 0.1

  # Signature bonus
  if binary.metadata.signature.isSome():
    score += 0.1

  return min(1.0, score)

# =============================================================================
# Binary Scoring
# =============================================================================

proc scoreBinary*(selector: BinarySelector, binary: BinaryCacheEntry,
                 system: SystemInfo): BinaryScore =
  ## Calculate comprehensive score for binary
  let compatScore = selector.calculateCompatibilityScore(binary, system)
  let perfScore = calculatePerformanceScore(binary, system)
  let sizeScore = calculateSizeScore(binary)
  let ageScore = calculateAgeScore(binary)
  let trustScore = calculateTrustScore(binary)

  # Calculate weighted overall score
  var overallScore = 0.0
  var totalWeight = 0.0

  for criteria, weight in selector.preferences.criteriaWeights.pairs:
    totalWeight += weight
    case criteria:
    of CriteriaCompatibility: overallScore += compatScore * weight
    of CriteriaPerformance: overallScore += perfScore * weight
    of CriteriaSize: overallScore += sizeScore * weight
    of CriteriaAge: overallScore += ageScore * weight
    of CriteriaTrust: overallScore += trustScore * weight

  if totalWeight > 0:
    overallScore = overallScore / totalWeight

  # Apply strategy-specific adjustments
  case selector.preferences.strategy:
  of StrategyPerformance:
    overallScore = overallScore * 0.7 + perfScore * 0.3
  of StrategySize:
    overallScore = overallScore * 0.7 + sizeScore * 0.3
  of StrategyConservative:
    overallScore = overallScore * 0.7 + compatScore * 0.3
  of StrategyLatest:
    overallScore = overallScore * 0.7 + ageScore * 0.3
  of StrategyOptimal:
    # No adjustment - use weighted score as-is
    discard

  var reasons: seq[string] = @[]

  # Add scoring reasons
  if compatScore >= 0.9:
    reasons.add("Excellent compatibility")
  elif compatScore >= 0.7:
    reasons.add("Good compatibility")
  else:
    reasons.add("Limited compatibility")

  if perfScore >= 0.8:
    reasons.add("High performance optimizations")
  elif perfScore >= 0.6:
    reasons.add("Moderate optimizations")

  if sizeScore >= 0.8:
    reasons.add("Compact binary")
  elif sizeScore <= 0.4:
    reasons.add("Large binary")

  if trustScore >= 0.8:
    reasons.add("High trust score")

  BinaryScore(
    binary: binary,
    compatibilityScore: compatScore,
    performanceScore: perfScore,
    sizeScore: sizeScore,
    ageScore: ageScore,
    trustScore: trustScore,
    overallScore: overallScore,
    reasons: reasons
  )

# =============================================================================
# Binary Selection
# =============================================================================

proc selectOptimalBinary*(selector: BinarySelector, candidates: seq[BinaryCacheEntry],
                         system: SystemInfo): SelectionResult =
  ## Select the optimal binary from candidates
  try:
    if candidates.len == 0:
      return SelectionResult(
        success: false,
        error: "No binary candidates available",
        errorCode: 404,
        fallbackToSource: selector.preferences.allowFallback
      )

    # Score all candidates
    var scores: seq[BinaryScore] = @[]
    for binary in candidates:
      let score = selector.scoreBinary(binary, system)

      # Filter by minimum compatibility score
      if score.compatibilityScore >= selector.preferences.minCompatibilityScore:
        scores.add(score)

    if scores.len == 0:
      return SelectionResult(
        success: false,
        error: fmt"No binaries meet minimum compatibility score ({selector.preferences.minCompatibilityScore:.3f})",
        errorCode: 406,
        fallbackToSource: selector.preferences.allowFallback
      )

    # Sort by overall score (highest first)
    scores.sort(proc(a, b: BinaryScore): int = cmp(b.overallScore, a.overallScore))

    # Apply additional selection logic
    let selectedScore = applySelectionStrategy(selector, scores, system)

    return SelectionResult(
      success: true,
      selectedBinary: selectedScore.binary,
      score: selectedScore,
      alternatives: scores[1..min(4, scores.high)]  # Up to 4 alternatives
    )

  except Exception as e:
    return SelectionResult(
      success: false,
      error: fmt"Binary selection failed: {e.msg}",
      errorCode: 500,
      fallbackToSource: selector.preferences.allowFallback
    )

proc applySelectionStrategy*(selector: BinarySelector, scores: seq[BinaryScore],
                           system: SystemInfo): BinaryScore =
  ## Apply strategy-specific selection logic
  if scores.len == 0:
    raise newException(ValueError, "No scores provided")

  case selector.preferences.strategy:
  of StrategyOptimal:
    # Return highest overall score
    return scores[0]

  of StrategyPerformance:
    # Find best performance score among top compatibility matches
    let topCompatible = scores.filterIt(it.compatibilityScore >= 0.8)
    if topCompatible.len > 0:
      return topCompatible.maxByIt(it.performanceScore)
    else:
      return scores[0]

  of StrategySize:
    # Find smallest binary among compatible ones
    let compatible = scores.filterIt(it.compatibilityScore >= 0.7)
    if compatible.len > 0:
      return compatible.maxByIt(it.sizeScore)
    else:
      return scores[0]

  of StrategyConservative:
    # Find most compatible binary
    return scores.maxByIt(it.compatibilityScore)

  of StrategyLatest:
    # Find newest binary among compatible ones
    let compatible = scores.filterIt(it.compatibilityScore >= 0.7)
    if compatible.len > 0:
      return compatible.maxByIt(it.ageScore)
    else:
      return scores[0]

# =============================================================================
# Fallback Decision Making
# =============================================================================

proc shouldFallbackToSource*(selector: BinarySelector, candidates: seq[BinaryCacheEntry],
                            system: SystemInfo): tuple[fallback: bool, reason: string] =
  ## Determine if we should fallback to source build
  if not selector.preferences.allowFallback:
    return (false, "Fallback disabled")

  if candidates.len == 0:
    return (true, "No binary candidates available")

  # Check if any candidates meet minimum requirements
  var hasCompatible = false
  for binary in candidates:
    let compatScore = selector.calculateCompatibilityScore(binary, system)
    if compatScore >= selector.preferences.minCompatibilityScore:
      hasCompatible = true
      break

  if not hasCompatible:
    return (true, "No compatible binaries found")

  # Check size constraints
  if selector.preferences.maxSizeIncrease > 0:
    let baseSizeEstimate = 10 * 1024 * 1024  # 10MB base estimate
    for binary in candidates:
      let sizeRatio = binary.size.float / baseSizeEstimate.float
      if sizeRatio > selector.preferences.maxSizeIncrease:
        continue  # This binary is too large, but others might be OK

  return (false, "Compatible binaries available")

# =============================================================================
# Selection Reporting
# =============================================================================

proc formatSelectionReport*(result: SelectionResult): string =
  ## Format selection result as human-readable report
  if not result.success:
    var report = fmt"Binary selection failed: {result.error}\n"
    if result.fallbackToSource:
      report.add("Recommendation: Build from source\n")
    return report

  let score = result.score
  var report = fmt"Selected binary: {score.binary.cacheKey}\n"
  report.add(fmt"Overall score: {score.overallScore:.3f}\n")
  report.add(fmt"Compatibility: {score.compatibilityScore:.3f}\n")
  report.add(fmt"Performance: {score.performanceScore:.3f}\n")
  report.add(fmt"Size: {formatFileSize(score.binary.size)}\n")
  report.add(fmt"Platform: {score.binary.platform}\n")

  if score.reasons.len > 0:
    report.add("Reasons:\n")
    for reason in score.reasons:
      report.add(fmt"  • {reason}\n")

  if result.alternatives.len > 0:
    report.add(fmt"\nAlternatives ({result.alternatives.len}):\n")
    for i, alt in result.alternatives:
      report.add(fmt"  {i+1}. {alt.binary.cacheKey} (score: {alt.overallScore:.3f})\n")

  return report

proc getSelectionStatistics*(results: seq[SelectionResult]): JsonNode =
  ## Get statistics about selection results
  var successful = 0
  var fallbacks = 0
  var totalScore = 0.0
  var strategies = initTable[string, int]()

  for result in results:
    if result.success:
      successful += 1
      totalScore += result.score.overallScore
    else:
      if result.fallbackToSource:
        fallbacks += 1

  return %*{
    "total_selections": results.len,
    "successful_selections": successful,
    "fallback_to_source": fallbacks,
    "success_rate": if results.len > 0: (successful.float / results.len.float) else: 0.0,
    "average_score": if successful > 0: (totalScore / successful.float) else: 0.0,
    "fallback_rate": if results.len > 0: (fallbacks.float / results.len.float) else: 0.0
  }

# =============================================================================
# Selection Preferences Management
# =============================================================================

proc updateSelectionPreferences*(selector: var BinarySelector,
                                preferences: SelectionPreferences) =
  ## Update selection preferences
  selector.preferences = preferences

proc setSelectionStrategy*(selector: var BinarySelector, strategy: SelectionStrategy) =
  ## Set selection strategy
  selector.preferences.strategy = strategy

proc setCriteriaWeight*(selector: var BinarySelector, criteria: SelectionCriteria, weight: float) =
  ## Set weight for selection criteria
  selector.preferences.criteriaWeights[criteria] = weight

proc getOptimizedPreferences*(system: SystemInfo): SelectionPreferences =
  ## Get preferences optimized for specific system
  var prefs = getDefaultSelectionPreferences()

  # Adjust for system characteristics
  case system.architecture:
  of ArchX86_64:
    prefs.criteriaWeights[CriteriaPerformance] = 0.9  # x86_64 benefits from optimizations
  of ArchAarch64:
    prefs.criteriaWeights[CriteriaSize] = 0.5         # ARM often size-constrained
  else:
    prefs.criteriaWeights[CriteriaCompatibility] = 1.0 # Conservative for other archs

  # Adjust for libc
  case system.libc:
  of LibcMusl:
    prefs.minCompatibilityScore = 0.8  # Be more strict with musl
  of LibcGlibc:
    prefs.minCompatibilityScore = 0.7  # More lenient with glibc
  else:
    prefs.minCompatibilityScore = 0.9  # Very strict for other libc

  return prefs

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

export SelectionCriteria, SelectionStrategy, SelectionPreferences
export BinaryScore, SelectionResult, BinarySelector
export newBinarySelector, getDefaultSelectionPreferences
export calculateCompatibilityScore, calculatePerformanceScore
export calculateSizeScore, calculateAgeScore, calculateTrustScore
export scoreBinary, selectOptimalBinary, applySelectionStrategy
export shouldFallbackToSource, formatSelectionReport, getSelectionStatistics
export updateSelectionPreferences, setSelectionStrategy, setCriteriaWeight
export getOptimizedPreferences