///|
/// An exact permitted change: literal path segments and added/removed/changed.
/// Rules do not match descendants and never contain configuration values.
pub(all) struct ConfigChangeRule {
  path : Array[String]
  kind : String
} derive(Debug, Eq, ToJson)

///|
pub extend ConfigChangeRule with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend ConfigChangeRule with Eq::{not_equal, equal}

///|
pub extend ConfigChangeRule with ToJson::{to_json}

///|
pub(all) struct ConfigChangeReview {
  accepted : Bool
  changes : Array[ConfigChange]
  unexpected : Array[ConfigChange]
} derive(Debug, Eq, ToJson)

///|
pub extend ConfigChangeReview with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend ConfigChangeReview with Eq::{not_equal, equal}

///|
pub extend ConfigChangeReview with ToJson::{to_json}

///|
fn change_rule_key(path : Array[String], kind : String) -> String {
  // JSON segments distinguish a literal dotted key from a nested path.
  kind + ":" + path.to_json().stringify()
}

///|
/// Review resolved configuration changes against an exact allow-list.
/// An empty policy accepts only semantic equality. No prefix or wildcard match
/// is performed, so allowing a leaf edit cannot allow removal of its parent.
/// Unused rules are allowed; this is a permission check, not a required-change
/// assertion. Values and deployment safety remain the caller's responsibility.
pub fn review_config_changes(
  before : Value,
  after : Value,
  rules : Array[ConfigChangeRule],
) -> ConfigChangeReview raise ParseError {
  if rules.length() > 1024 {
    raise Invalid("configuration change policy exceeds 1024 rules")
  }
  let allowed : Map[String, Bool] = Map([])
  let mut characters = 0
  for rule in rules {
    if rule.path.is_empty() || rule.path.length() > 32 {
      raise Invalid(
        "configuration change policy path must have 1 to 32 segments",
      )
    }
    if rule.kind != "added" && rule.kind != "removed" && rule.kind != "changed" {
      raise Invalid("invalid configuration change policy kind")
    }
    for segment in rule.path {
      if segment.length() > 65536 - characters {
        raise Invalid("configuration change policy exceeds character limit")
      }
      characters += segment.length()
    }
    let key = change_rule_key(rule.path, rule.kind)
    if allowed.contains(key) {
      raise Invalid("duplicate configuration change policy rule")
    }
    allowed[key] = true
  }
  let changes = diff_resolved_configs(before, after)
  let unexpected = changes.filter(change => {
    !allowed.contains(change_rule_key(change.path, change.kind))
  })
  { accepted: unexpected.is_empty(), changes, unexpected, }
}