///|
pub(all) enum RiskLevel {
  Low
  Medium
  High
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) enum RuleCondition {
  PathPattern(String)
  Extension(String)
  MinimumSize(Int64)
  OlderThan(Int64)
  EmptyDirectory
  Any(Array[RuleCondition])
  All(Array[RuleCondition])
  Not(RuleCondition)
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct CleanupRule {
  id : String
  title : String
  reason : String
  risk : RiskLevel
  regenerable : Bool
  condition : RuleCondition
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct RuleMatch {
  rule_id : String
  rule_title : String
  path : String
  size : Int64
  reason : String
  risk : RiskLevel
  regenerable : Bool
} derive(Eq, Debug, ToJson, FromJson)

///|
fn normalized_extension(value : String) -> String {
  let lower = value.to_lower()
  if lower.has_prefix(".") {
    lower[1:].to_owned()
  } else {
    lower
  }
}

///|
pub fn condition_matches(
  condition : RuleCondition,
  entry : Entry,
  snapshot : Snapshot,
  now : Int64,
) -> Bool {
  match condition {
    PathPattern(pattern) =>
      wildcard_match(normalize_relative_path(pattern), entry.path) ||
      wildcard_match(normalize_relative_path(pattern), entry.name())
    Extension(extension) =>
      entry.is_file() && entry.extension() == normalized_extension(extension)
    MinimumSize(bytes) => entry.is_file() && entry.size >= bytes
    OlderThan(seconds) =>
      entry.is_file() &&
      now >= entry.modified_at &&
      now - entry.modified_at >= seconds
    EmptyDirectory =>
      if !entry.is_dir() {
        false
      } else {
        match stat_for_directory(snapshot, entry.path) {
          Some(stat) => stat.recursive_files == 0
          None => true
        }
      }
    Any(conditions) => {
      for item in conditions {
        if condition_matches(item, entry, snapshot, now) {
          return true
        }
      }
      false
    }
    All(conditions) => {
      for item in conditions {
        if !condition_matches(item, entry, snapshot, now) {
          return false
        }
      }
      true
    }
    Not(inner) => !condition_matches(inner, entry, snapshot, now)
  }
}

///|
pub fn evaluate_rule(
  rule : CleanupRule,
  snapshot : Snapshot,
  now : Int64,
) -> Array[RuleMatch] {
  let result : Array[RuleMatch] = []
  for entry in snapshot.entries {
    if condition_matches(rule.condition, entry, snapshot, now) {
      result.push(RuleMatch::{
        rule_id: rule.id,
        rule_title: rule.title,
        path: entry.path,
        size: if entry.is_file() {
          entry.size
        } else if entry.is_dir() {
          match stat_for_directory(snapshot, entry.path) {
            Some(stat) => stat.recursive_size
            None => 0L
          }
        } else {
          0L
        },
        reason: rule.reason,
        risk: rule.risk,
        regenerable: rule.regenerable,
      })
    }
  }
  result
}

///|
pub fn evaluate_rules(
  rules : Array[CleanupRule],
  snapshot : Snapshot,
  now : Int64,
) -> Array[RuleMatch] {
  let result : Array[RuleMatch] = []
  let seen : Map[String, Bool] = Map([])
  for rule in rules {
    for candidate in evaluate_rule(rule, snapshot, now) {
      let key = candidate.rule_id + "\n" + candidate.path
      if seen.get(key) is None {
        seen[key] = true
        result.push(candidate)
      }
    }
  }
  result.sort_by(fn(a, b) {
    if a.size > b.size {
      -1
    } else if a.size < b.size {
      1
    } else if a.path < b.path {
      -1
    } else {
      1
    }
  })
  result
}

///|
pub fn builtin_cleanup_rules() -> Array[CleanupRule] {
  [
    CleanupRule::{
      id: "moonbit-build",
      title: "MoonBit build output",
      reason: "The _build directory can be regenerated by the MoonBit toolchain.",
      risk: Low,
      regenerable: true,
      condition: Any([PathPattern("_build"), PathPattern("*/_build")]),
    },
    CleanupRule::{
      id: "node-modules",
      title: "Node.js dependency directory",
      reason: "node_modules can normally be restored from the dependency lockfile.",
      risk: Low,
      regenerable: true,
      condition: Any([
        PathPattern("node_modules"),
        PathPattern("*/node_modules"),
      ]),
    },
    CleanupRule::{
      id: "python-cache",
      title: "Python bytecode cache",
      reason: "Python recreates __pycache__ directories when modules are imported.",
      risk: Low,
      regenerable: true,
      condition: Any([PathPattern("__pycache__"), PathPattern("*/__pycache__")]),
    },
    CleanupRule::{
      id: "temporary-files",
      title: "Temporary file",
      reason: "The name or extension follows a common temporary-file convention.",
      risk: Medium,
      regenerable: false,
      condition: Any([
        Extension("tmp"),
        Extension("temp"),
        PathPattern("*~"),
        PathPattern("*.bak"),
      ]),
    },
    CleanupRule::{
      id: "old-logs",
      title: "Old log file",
      reason: "The file is a log and has not been modified for at least 90 days.",
      risk: Medium,
      regenerable: false,
      condition: All([Extension("log"), OlderThan(7776000L)]),
    },
    CleanupRule::{
      id: "empty-directory",
      title: "Empty directory",
      reason: "The directory contains no regular files in the snapshot.",
      risk: Medium,
      regenerable: false,
      condition: EmptyDirectory,
    },
  ]
}

///|
pub fn risk_name(risk : RiskLevel) -> String {
  match risk {
    Low => "low"
    Medium => "medium"
    High => "high"
  }
}