///|
/// Classifies the impact of a configuration change for version evolution.
///
/// This baseline classification treats added paths as compatible, changed
/// values as behavioral changes, and removed or type-changed paths as breaking
/// changes. A later policy layer can refine these defaults with schema rules.
pub(all) enum ConfigCompatibilityImpact {
  Compatible
  Behavioral
  Breaking
} derive(Eq, Debug)

///|
/// Selects which compatibility impacts should fail a release gate.
///
/// `BreakingOnly` is the default and rejects only breaking changes.
/// `NoBehavioralChanges` also rejects behavioral changes, while `NoChanges`
/// requires the two snapshots to be identical.
pub(all) enum ConfigCompatibilityPolicy {
  BreakingOnly
  NoBehavioralChanges
  NoChanges
} derive(Eq, Debug)

///|
/// A configuration difference together with its compatibility impact.
pub struct ConfigCompatibilityChange {
  difference : ConfigDiff
  impact : ConfigCompatibilityImpact
} derive(Eq, Debug)

///|
/// The compatibility result for an earlier and a later configuration value.
pub struct ConfigCompatibilityReport {
  changes : Array[ConfigCompatibilityChange]
} derive(Eq, Debug)

///|
/// Return a stable lowercase name for a compatibility impact.
pub fn ConfigCompatibilityImpact::to_string(
  self : ConfigCompatibilityImpact,
) -> String {
  match self {
    Compatible => "compatible"
    Behavioral => "behavioral"
    Breaking => "breaking"
  }
}

///|
/// Return the stable command-line name for a compatibility policy.
pub fn ConfigCompatibilityPolicy::to_string(
  self : ConfigCompatibilityPolicy,
) -> String {
  match self {
    BreakingOnly => "breaking"
    NoBehavioralChanges => "behavioral"
    NoChanges => "any"
  }
}

///|
/// Return whether an impact is allowed by this policy.
fn ConfigCompatibilityPolicy::allows(
  self : ConfigCompatibilityPolicy,
  impact : ConfigCompatibilityImpact,
) -> Bool {
  match self {
    BreakingOnly => impact != Breaking
    NoBehavioralChanges => impact == Compatible
    NoChanges => false
  }
}

///|
/// Return the path affected by this compatibility change.
pub fn ConfigCompatibilityChange::path(
  self : ConfigCompatibilityChange,
) -> ConfigPath {
  self.difference.path()
}

///|
/// Return the original difference category.
pub fn ConfigCompatibilityChange::difference_kind(
  self : ConfigCompatibilityChange,
) -> ConfigDiffKind {
  self.difference.kind()
}

///|
/// Return the compatibility impact assigned to this change.
pub fn ConfigCompatibilityChange::impact(
  self : ConfigCompatibilityChange,
) -> ConfigCompatibilityImpact {
  self.impact
}

///|
/// Return the earlier value, or `None` when the path was added.
pub fn ConfigCompatibilityChange::before(
  self : ConfigCompatibilityChange,
) -> ConfigValue? {
  self.difference.before()
}

///|
/// Return the later value, or `None` when the path was removed.
pub fn ConfigCompatibilityChange::after(
  self : ConfigCompatibilityChange,
) -> ConfigValue? {
  self.difference.after()
}

///|
/// Render a concise description of the change.
pub fn ConfigCompatibilityChange::message(
  self : ConfigCompatibilityChange,
) -> String {
  let path = self.path().to_string()
  let location = if path == "" {
    "configuration root"
  } else {
    "configuration path '\{path}'"
  }
  match self.difference_kind() {
    Added => "\{location} was added"
    Removed => "\{location} was removed"
    Changed => "\{location} changed value"
    TypeChanged => "\{location} changed type"
  }
}

///|
/// Return one change by its stable path order, or `None` when out of bounds.
pub fn ConfigCompatibilityReport::change(
  self : ConfigCompatibilityReport,
  index : Int,
) -> ConfigCompatibilityChange? {
  self.changes.get(index)
}

///|
/// Return all changes in stable path order.
pub fn ConfigCompatibilityReport::changes(
  self : ConfigCompatibilityReport,
) -> Array[ConfigCompatibilityChange] {
  self.changes.copy()
}

///|
/// Return the total number of configuration changes.
pub fn ConfigCompatibilityReport::change_count(
  self : ConfigCompatibilityReport,
) -> Int {
  self.changes.length()
}

///|
/// Return the number of compatible changes.
pub fn ConfigCompatibilityReport::compatible_count(
  self : ConfigCompatibilityReport,
) -> Int {
  self.changes.fold(init=0, (count, change) => {
    if change.impact() == Compatible {
      count + 1
    } else {
      count
    }
  })
}

///|
/// Return the number of behavioral changes.
pub fn ConfigCompatibilityReport::behavioral_count(
  self : ConfigCompatibilityReport,
) -> Int {
  self.changes.fold(init=0, (count, change) => {
    if change.impact() == Behavioral {
      count + 1
    } else {
      count
    }
  })
}

///|
/// Return the number of breaking changes.
pub fn ConfigCompatibilityReport::breaking_count(
  self : ConfigCompatibilityReport,
) -> Int {
  self.changes.fold(init=0, (count, change) => {
    if change.impact() == Breaking {
      count + 1
    } else {
      count
    }
  })
}

///|
/// Return the number of changes that violate a release policy.
pub fn ConfigCompatibilityReport::violation_count(
  self : ConfigCompatibilityReport,
  policy : ConfigCompatibilityPolicy,
) -> Int {
  self.changes.fold(init=0, (count, change) => {
    if policy.allows(change.impact()) {
      count
    } else {
      count + 1
    }
  })
}

///|
/// Return whether this report passes a release policy.
pub fn ConfigCompatibilityReport::passes(
  self : ConfigCompatibilityReport,
  policy : ConfigCompatibilityPolicy,
) -> Bool {
  self.violation_count(policy) == 0
}

///|
/// Return whether this report contains no breaking changes.
pub fn ConfigCompatibilityReport::is_compatible(
  self : ConfigCompatibilityReport,
) -> Bool {
  self.passes(BreakingOnly)
}

///|
/// Classify one value-level difference using the baseline compatibility model.
fn compatibility_impact(kind : ConfigDiffKind) -> ConfigCompatibilityImpact {
  match kind {
    Added => Compatible
    Removed => Breaking
    Changed => Behavioral
    TypeChanged => Breaking
  }
}

///|
/// Compare two configuration snapshots for version compatibility.
///
/// This API intentionally compares already materialized JSON-compatible
/// values. It does not parse configuration files or merge runtime layers.
/// Differences retain the deterministic path order of `ConfigValue::diff`.
pub fn ConfigValue::compatibility_with(
  self : ConfigValue,
  later : ConfigValue,
) -> ConfigCompatibilityReport {
  let changes = self
    .diff(later)
    .map(diff => { difference: diff, impact: compatibility_impact(diff.kind()) })
  { changes, }
}