///|
pub(all) struct RuleSet {
  format_version : Int
  rules : Array[CleanupRule]
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) enum RuleConfigIssueKind {
  EmptyId
  DuplicateId
  EmptyTitle
  InvalidMinimumSize
  InvalidAge
  EmptyConditionGroup
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct RuleConfigIssue {
  kind : RuleConfigIssueKind
  rule_id : String
  message : String
} derive(Eq, Debug, ToJson, FromJson)

///|
fn validate_condition(
  condition : RuleCondition,
  rule_id : String,
  issues : Array[RuleConfigIssue],
) -> Unit {
  match condition {
    MinimumSize(bytes) =>
      if bytes < 0L {
        issues.push(RuleConfigIssue::{
          kind: InvalidMinimumSize,
          rule_id,
          message: "MinimumSize must be zero or greater.",
        })
      }
    OlderThan(seconds) =>
      if seconds < 0L {
        issues.push(RuleConfigIssue::{
          kind: InvalidAge,
          rule_id,
          message: "OlderThan must be zero or greater.",
        })
      }
    Any(conditions) | All(conditions) => {
      if conditions.is_empty() {
        issues.push(RuleConfigIssue::{
          kind: EmptyConditionGroup,
          rule_id,
          message: "Any and All condition groups cannot be empty.",
        })
      }
      for child in conditions {
        validate_condition(child, rule_id, issues)
      }
    }
    Not(child) => validate_condition(child, rule_id, issues)
    PathPattern(_) | Extension(_) | EmptyDirectory => ()
  }
}

///|
pub fn validate_rule_set(rule_set : RuleSet) -> Array[RuleConfigIssue] {
  let issues : Array[RuleConfigIssue] = []
  let ids : Map[String, Bool] = Map([])
  for rule in rule_set.rules {
    if rule.id.trim().is_empty() {
      issues.push(RuleConfigIssue::{
        kind: EmptyId,
        rule_id: rule.id,
        message: "Every cleanup rule needs a non-empty id.",
      })
    } else if ids.get(rule.id) is Some(_) {
      issues.push(RuleConfigIssue::{
        kind: DuplicateId,
        rule_id: rule.id,
        message: "Cleanup rule ids must be unique.",
      })
    } else {
      ids[rule.id] = true
    }
    if rule.title.trim().is_empty() {
      issues.push(RuleConfigIssue::{
        kind: EmptyTitle,
        rule_id: rule.id,
        message: "Every cleanup rule needs a title.",
      })
    }
    validate_condition(rule.condition, rule.id, issues)
  }
  issues
}

///|
pub fn default_rule_set() -> RuleSet {
  RuleSet::{ format_version: 1, rules: builtin_cleanup_rules(), }
}

///|
pub fn rule_set_to_json(rule_set : RuleSet, pretty? : Bool = true) -> String {
  let json = ToJson::to_json(rule_set)
  if pretty {
    json.stringify(indent=2)
  } else {
    json.stringify()
  }
}

///|
pub fn rule_set_from_json(source : String) -> RuleSet raise {
  let rule_set : RuleSet = @json.from_json(@json.parse(source))
  if rule_set.format_version != 1 {
    raise Failure("Unsupported cleanup rule format version")
  }
  let issues = validate_rule_set(rule_set)
  if !issues.is_empty() {
    raise Failure(
      "Invalid cleanup rules: " + issues[0].rule_id + ": " + issues[0].message,
    )
  }
  rule_set
}

///|
pub async fn write_rule_set(path : String, rule_set : RuleSet) -> Unit {
  @fs.write_file(path, rule_set_to_json(rule_set))
}

///|
pub async fn read_rule_set(path : String) -> RuleSet {
  rule_set_from_json(@fs.read_file(path).text())
}