///|
pub(all) struct GrowthPolicy {
  format_version : Int
  maximum_net_growth_bytes : Int64
  maximum_added_files : Int
  maximum_removed_files : Int
  maximum_changed_files : Int
  maximum_single_file_growth_bytes : Int64
  protected_patterns : Array[String]
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) enum GrowthViolationKind {
  NetGrowthExceeded
  AddedFilesExceeded
  RemovedFilesExceeded
  ChangedFilesExceeded
  SingleFileGrowthExceeded
  ProtectedPathChanged
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct GrowthViolation {
  kind : GrowthViolationKind
  path : String
  actual : Int64
  limit : Int64
  message : String
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct GrowthPolicyResult {
  net_growth_bytes : Int64
  added_files : Int
  removed_files : Int
  changed_files : Int
  largest_file_growth_bytes : Int64
  largest_file_growth_path : String
  violations : Array[GrowthViolation]
} derive(Eq, Debug, ToJson, FromJson)

///|
pub fn GrowthPolicy::default() -> GrowthPolicy {
  GrowthPolicy::{
    format_version: 1,
    maximum_net_growth_bytes: 512L * 1024L * 1024L,
    maximum_added_files: 10000,
    maximum_removed_files: 10000,
    maximum_changed_files: 10000,
    maximum_single_file_growth_bytes: 256L * 1024L * 1024L,
    protected_patterns: ["src/*", "LICENSE", "README*"],
  }
}

///|
pub fn validate_growth_policy(policy : GrowthPolicy) -> Array[String] {
  let issues : Array[String] = []
  if policy.format_version != 1 {
    issues.push("Unsupported growth policy version.")
  }
  if policy.maximum_net_growth_bytes < 0L {
    issues.push("Maximum net growth cannot be negative.")
  }
  if policy.maximum_added_files < 0 {
    issues.push("Maximum added-file count cannot be negative.")
  }
  if policy.maximum_removed_files < 0 {
    issues.push("Maximum removed-file count cannot be negative.")
  }
  if policy.maximum_changed_files < 0 {
    issues.push("Maximum changed-file count cannot be negative.")
  }
  if policy.maximum_single_file_growth_bytes < 0L {
    issues.push("Maximum single-file growth cannot be negative.")
  }
  for pattern in policy.protected_patterns {
    if pattern.trim().is_empty() {
      issues.push("Protected path patterns cannot be empty.")
    }
  }
  issues
}

///|
fn growth_violation(
  kind : GrowthViolationKind,
  path : String,
  actual : Int64,
  limit : Int64,
  message : String,
) -> GrowthViolation {
  GrowthViolation::{ kind, path, actual, limit, message, }
}

///|
fn changed_regular_file(change : EntryChange) -> Bool {
  match (change.before, change.after) {
    (Some(before), _) if before.is_file() => true
    (_, Some(after)) if after.is_file() => true
    _ => false
  }
}

///|
pub fn evaluate_growth_policy(
  before : Snapshot,
  after : Snapshot,
  policy : GrowthPolicy,
) -> GrowthPolicyResult raise {
  let policy_issues = validate_growth_policy(policy)
  if !policy_issues.is_empty() {
    raise Failure("Invalid growth policy: " + policy_issues[0])
  }
  let diff = compare_snapshots(before, after)
  let added = diff.added_count()
  let removed = diff.removed_count()
  let changed = diff.changed_count()
  let violations : Array[GrowthViolation] = []
  if diff.net_size > policy.maximum_net_growth_bytes {
    violations.push(
      growth_violation(
        NetGrowthExceeded,
        "",
        diff.net_size,
        policy.maximum_net_growth_bytes,
        "Net storage growth exceeded the configured limit.",
      ),
    )
  }
  if added > policy.maximum_added_files {
    violations.push(
      growth_violation(
        AddedFilesExceeded,
        "",
        added.to_int64(),
        policy.maximum_added_files.to_int64(),
        "Added-file count exceeded the configured limit.",
      ),
    )
  }
  if removed > policy.maximum_removed_files {
    violations.push(
      growth_violation(
        RemovedFilesExceeded,
        "",
        removed.to_int64(),
        policy.maximum_removed_files.to_int64(),
        "Removed-file count exceeded the configured limit.",
      ),
    )
  }
  if changed > policy.maximum_changed_files {
    violations.push(
      growth_violation(
        ChangedFilesExceeded,
        "",
        changed.to_int64(),
        policy.maximum_changed_files.to_int64(),
        "Changed-file count exceeded the configured limit.",
      ),
    )
  }
  let mut largest_growth = 0L
  let mut largest_path = ""
  for change in diff.changes {
    if changed_regular_file(change) && change.size_delta > largest_growth {
      largest_growth = change.size_delta
      largest_path = change.path
    }
    if changed_regular_file(change) &&
      change.size_delta > policy.maximum_single_file_growth_bytes {
      violations.push(
        growth_violation(
          SingleFileGrowthExceeded,
          change.path,
          change.size_delta,
          policy.maximum_single_file_growth_bytes,
          "A file grew beyond the configured per-file limit.",
        ),
      )
    }
    if changed_regular_file(change) &&
      matches_any_pattern(change.path, policy.protected_patterns) {
      violations.push(
        growth_violation(
          ProtectedPathChanged,
          change.path,
          change.size_delta,
          0L,
          "A protected path changed between snapshots.",
        ),
      )
    }
  }
  GrowthPolicyResult::{
    net_growth_bytes: diff.net_size,
    added_files: added,
    removed_files: removed,
    changed_files: changed,
    largest_file_growth_bytes: largest_growth,
    largest_file_growth_path: largest_path,
    violations,
  }
}

///|
pub fn render_growth_policy_result(result : GrowthPolicyResult) -> String {
  let lines : Array[String] = [
    "MoonDisk growth policy check",
    "Net growth: " + format_bytes(result.net_growth_bytes),
    "Added files: " + result.added_files.to_string(),
    "Removed files: " + result.removed_files.to_string(),
    "Changed files: " + result.changed_files.to_string(),
    "Largest file growth: " +
    format_bytes(result.largest_file_growth_bytes) +
    (if result.largest_file_growth_path == "" {
      ""
    } else {
      " (" + result.largest_file_growth_path + ")"
    }),
    "Violations: " + result.violations.length().to_string(),
  ]
  for violation in result.violations {
    let location = if violation.path == "" {
      ""
    } else {
      " [" + violation.path + "]"
    }
    lines.push("- " + violation.message + location)
  }
  if result.violations.is_empty() {
    lines.push("Result: approved.")
  } else {
    lines.push("Result: rejected.")
  }
  lines.join("\n")
}

///|
pub fn growth_policy_to_json(policy : GrowthPolicy) -> String {
  ToJson::to_json(policy).stringify(indent=2)
}

///|
pub fn growth_policy_from_json(source : String) -> GrowthPolicy raise {
  let policy : GrowthPolicy = @json.from_json(@json.parse(source))
  let issues = validate_growth_policy(policy)
  if !issues.is_empty() {
    raise Failure("Invalid growth policy: " + issues[0])
  }
  policy
}

///|
pub fn growth_policy_result_to_json(result : GrowthPolicyResult) -> String {
  ToJson::to_json(result).stringify(indent=2)
}

///|
pub async fn write_growth_policy(path : String, policy : GrowthPolicy) -> Unit {
  @fs.write_file(path, growth_policy_to_json(policy))
}

///|
pub async fn read_growth_policy(path : String) -> GrowthPolicy {
  growth_policy_from_json(@fs.read_file(path).text())
}